Files
GhomandGitHub 6eb2d1674a Implementing materials checks for techweb designs. (#96257)
## About The Pull Request
In similar fashion to what was done with crafting recipes last year,
this year it's time for techweb designs and printed items to be audited.
This is mostly just about consistency, we've a lot of items that can be
printed by protolathes, autolathes, circuit printers and techfab etc.
However they almost all (except most stacks, mainly) have custom
materials that do not match in one way or another with the materials
used by the design, which is what this PR is for.

"But items printed from lathes etc. already get the mats used to make
them." Yes, they do, however that isn't the case for items of the same
type that were spawned in some other way (cargo shuttle, space/maints
loot, mapped, admins), this create a subtle discrepancy. It isn't a huge
deal (in spite of the size of this PR, ton of designs), but given that I
have done something similar with crafting recipes before, I may as well
give it a second arc of some sort and bring things to completion. And
fix a few possible oversights.

TL;DR consistency and stuff

## Why It's Good For The Game
Consistency, unit test checks to make it harder not to be consistent in
the future. Still has a few TODOs like:

- [x] Fixed newly printed, fully charged RCDs costing less than the RCD
cartridges required to fully charge one. EDIT: I had to tweak the newly
added RDD as well because it suffered from the same fundamental issue.
- [x] Fixed plates being made of iron and yet shattering like ceramic
ones. A new subtype for metallic ones has been made.

## Changelog

🆑
refactor: Refactored a few things with techweb designs (the ones for
autolathes, protolathes, circuit printers, mechfabs etc.) to make sure
that the materials of items that can be made from these designs more
closely match the materials used to make them.
fix: Lizard fries no longer need a plate to be made, like all other
treats that used to require plates in a distant past.
balance: Tweaked the materials cost of RCD, RDD and RCD cartridges.
image: Oven trays now have a more metallic hue.
balance: Plates printed printed from lathes won't shatter like ceramic
ones, in virtue of them being made out of iron instead.
/🆑
2026-07-06 00:13:02 -07:00

385 lines
14 KiB
Plaintext

// Light Replacer (LR)
//
// ABOUT THE DEVICE
//
// This is a device supposedly to be used by Janitors and Janitor Cyborgs which will
// allow them to easily replace lights. This was mostly designed for Janitor Cyborgs since
// they don't have hands or a way to replace lightbulbs.
//
// HOW IT WORKS
//
// You attack a light fixture with it, if the light fixture is broken it will replace the
// light fixture with a working light; the broken light is then placed on the floor for the
// user to then pickup with a trash bag. If it's empty then it will just place a light in the fixture.
//
// HOW TO REFILL THE DEVICE
//
// It will need to be manually refilled with lights.
// If it's part of a robot module, it will charge when the Robot is inside a Recharge Station.
//
// EMAGGED FEATURES
//
// I'm not sure everyone will react the emag's features so please say what your opinions are of it. (I'm pretty sure the players like it)
//
// When emagged it will rig every light it replaces with plasma, which will slowly heat up and ignite while the light is on.
// This is VERY noticable, even the device's name changes when you emag it so if anyone
// examines you when you're holding it in your hand, you will be discovered.
//
#define GLASS_SHEET_USES 5
#define LIGHTBULB_COST 1
#define BULB_SHARDS_REQUIRED 4
/obj/item/lightreplacer
name = "light replacer"
desc = "A device to automatically replace lights. Refill with broken or working light bulbs, or sheets of glass."
icon = 'icons/obj/service/janitor.dmi'
icon_state = "lightreplacer"
inhand_icon_state = "electronic"
worn_icon_state = "light_replacer"
lefthand_file = 'icons/mob/inhands/items/devices_lefthand.dmi'
righthand_file = 'icons/mob/inhands/items/devices_righthand.dmi'
w_class = WEIGHT_CLASS_SMALL
obj_flags = CONDUCTS_ELECTRICITY
slot_flags = ITEM_SLOT_BELT
force = 8
custom_materials = list(/datum/material/glass = SHEET_MATERIAL_AMOUNT * 1.5, /datum/material/iron = SHEET_MATERIAL_AMOUNT * 0.75, /datum/material/silver = SMALL_MATERIAL_AMOUNT * 1.5)
/// How many uses does our light replacer have?
var/uses = 10
/// The maximum number of lights this replacer can hold
var/max_uses = 20
/// The light replacer's charge increment (used for adding to cyborg light replacers)
var/charge = 1
/// Eating used bulbs gives us bulb shards. Requires BULB_SHARDS_MAXIMUM to produce a new light.
var/bulb_shards = 0
/// whether it is "bluespace powered" (can be used at a range)
var/bluespace_toggle = FALSE
/obj/item/lightreplacer/examine(mob/user)
. = ..()
. += status_string()
/obj/item/lightreplacer/interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers)
return do_action(interacting_with, user) ? ITEM_INTERACT_SUCCESS : NONE
/obj/item/lightreplacer/ranged_interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers)
// has no bluespace capabilities
if(!bluespace_toggle)
return NONE
// target not in range
if(interacting_with.z != user.z)
return NONE
//replace lights & stuff
return do_action(interacting_with, user) ? ITEM_INTERACT_SUCCESS : NONE
/obj/item/lightreplacer/item_interaction(mob/living/user, obj/item/tool, list/modifiers)
if(uses >= max_uses)
user.balloon_alert(user, "already full!")
return ITEM_INTERACT_BLOCKING
if(istype(tool, /obj/item/stack/sheet/glass))
var/obj/item/stack/sheet/glass/glass_to_insert = tool
if(!glass_to_insert.use(LIGHTBULB_COST))
user.balloon_alert(user, "need [LIGHTBULB_COST] glass sheets!")
return ITEM_INTERACT_BLOCKING
add_uses(GLASS_SHEET_USES)
user.balloon_alert(user, "glass inserted")
return ITEM_INTERACT_SUCCESS
if(tool.type == /obj/item/shard) //we don't want to insert plasma, titanium or other types of shards
if(!user.temporarilyRemoveItemFromInventory(tool))
user.balloon_alert(user, "stuck in your hand!")
return ITEM_INTERACT_BLOCKING
if(!add_shard(user)) //add_shard will display a message if it created a bulb from the shard so only display message when that does not happen
user.balloon_alert(user, "shard inserted")
qdel(tool)
return ITEM_INTERACT_SUCCESS
if(istype(tool, /obj/item/light))
var/obj/item/light/light_to_insert = tool
//remove from player's hand
if(!user.temporarilyRemoveItemFromInventory(light_to_insert))
user.balloon_alert(user, "stuck in your hand!")
return ITEM_INTERACT_BLOCKING
//insert light. display message only if adding a shard did not create a new bulb else the messages will conflict
var/display_msg = TRUE
if(light_to_insert.status == LIGHT_OK)
add_uses(1)
else if(add_shard(user))
display_msg = FALSE
if(display_msg)
user.balloon_alert(user, "light inserted")
qdel(light_to_insert)
return ITEM_INTERACT_SUCCESS
if(istype(tool, /obj/item/storage))
var/replaced_something = FALSE
var/loaded = FALSE
for(var/obj/item/item_to_check in tool.contents)
//reached max capacity during insertion
if(src.uses >= max_uses)
break
//consume the item only if it's a light tube, bulb or shard
loaded = FALSE
if(istype(item_to_check, /obj/item/light))
var/obj/item/light/found_light = item_to_check
if(found_light.status == LIGHT_OK)
add_uses(1)
else
add_shard(user)
loaded = TRUE
else if(istype(item_to_check, /obj/item/stack/sheet/glass))
var/obj/item/stack/sheet/glass/glass_to_insert = item_to_check
if(glass_to_insert.use(LIGHTBULB_COST))
add_uses(GLASS_SHEET_USES)
loaded = TRUE
else if(item_to_check.type == /obj/item/shard)
add_shard(user)
loaded = TRUE
//if item was loaded delete it
if(loaded)
qdel(item_to_check)
replaced_something = TRUE
if(!replaced_something)
user.balloon_alert(user, "nothing usable in [tool]!")
return ITEM_INTERACT_BLOCKING
user.balloon_alert(user, "lights inserted")
return ITEM_INTERACT_SUCCESS
return NONE
/obj/item/lightreplacer/emag_act(mob/user, obj/item/card/emag/emag_card)
if(obj_flags & EMAGGED)
return FALSE
obj_flags |= EMAGGED
playsound(loc, SFX_SPARKS, 100, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
update_appearance()
to_chat(user, span_warning("[src]'s lights are now filled with plasma! Be careful to only install them in disabled light fixtures, lest they explode!"))
return FALSE
/obj/item/lightreplacer/update_name(updates)
. = ..()
name = (obj_flags & EMAGGED) ? "shortcircuited [initial(name)]" : initial(name)
/obj/item/lightreplacer/update_icon_state()
icon_state = "[initial(icon_state)][(obj_flags & EMAGGED ? "-emagged" : "")]"
return ..()
/obj/item/lightreplacer/vv_edit_var(vname, vval)
if(vname == NAMEOF(src, obj_flags))
update_appearance()
else if(vname == NAMEOF(src, uses))
uses = clamp(vval, 0, max_uses)
return TRUE
else if(vname == NAMEOF(src, max_uses))
if(vval <= 0)
return FALSE
max_uses = vval
uses = clamp(uses, 0, max_uses)
return TRUE
else if(vname == NAMEOF(src, bulb_shards))
if(vval <= 0)
return FALSE
add_uses(round(vval / BULB_SHARDS_REQUIRED))
bulb_shards = vval % BULB_SHARDS_REQUIRED
return TRUE
return ..()
/obj/item/lightreplacer/attack_self(mob/user)
var/on_a_light = FALSE //For if we are on the same tile as a light when we do this
for(var/obj/machinery/light/target in user.loc)
replace_light(target, user)
on_a_light = TRUE
if(!on_a_light) //So we don't give a balloon alert when we just used replace_light
user.balloon_alert(user, "[uses] lights, [bulb_shards]/[BULB_SHARDS_REQUIRED] fragments")
/**
* attempts to fix lights, flood lights & lights on a turf
* Arguments
* * target - the target we are trying to fix
* * user - the mob performing this action
* returns TRUE if the target was valid[light, floodlight or turf] regardless if any light's were fixed or not
*/
/obj/item/lightreplacer/proc/do_action(atom/target, mob/user)
// if we are attacking an light fixture then replace it directly
if(istype(target, /obj/machinery/light))
if(replace_light(target, user) && bluespace_toggle)
user.Beam(target, icon_state = "rped_upgrade", time = 0.5 SECONDS)
playsound(src, 'sound/items/pshoom/pshoom.ogg', 40, 1)
return TRUE
// if we are attacking a floodlight frame finish it
if(istype(target, /obj/structure/floodlight_frame))
var/obj/structure/floodlight_frame/frame = target
if(frame.state == FLOODLIGHT_NEEDS_LIGHTS && Use(user))
new /obj/machinery/power/floodlight(frame.loc)
if(bluespace_toggle)
user.Beam(target, icon_state = "rped_upgrade", time = 0.5 SECONDS)
playsound(src, 'sound/items/pshoom/pshoom.ogg', 40, 1)
to_chat(user, span_notice("You finish \the [frame] with a light tube."))
qdel(frame)
return TRUE
//attempt to replace all light sources on the turf
if(isturf(target))
var/light_replaced = FALSE
for(var/atom/target_atom in target)
if(replace_light(target_atom, user))
light_replaced = TRUE
if(light_replaced && bluespace_toggle)
user.Beam(target, icon_state = "rped_upgrade", time = 0.5 SECONDS)
playsound(src, 'sound/items/pshoom/pshoom.ogg', 40, 1)
return TRUE
return FALSE
/obj/item/lightreplacer/proc/status_string()
return "It has [uses] light\s remaining (plus [bulb_shards]/[BULB_SHARDS_REQUIRED] fragment\s)."
/obj/item/lightreplacer/proc/Use(mob/user)
if(uses <= 0)
return FALSE
playsound(src.loc, 'sound/machines/click.ogg', 50, TRUE)
src.add_fingerprint(user)
add_uses(-1)
return TRUE
// Negative numbers will subtract
/obj/item/lightreplacer/proc/add_uses(amount = 1)
uses = clamp(uses + amount, 0, max_uses)
/obj/item/lightreplacer/proc/add_shard(user)
bulb_shards += 1
if(bulb_shards >= BULB_SHARDS_REQUIRED)
bulb_shards = 0
add_uses(1)
to_chat(user, span_notice("\The [src] fabricates a new bulb from the broken glass it has stored. [status_string()]"))
playsound(src.loc, 'sound/machines/ding.ogg', 50, TRUE)
return TRUE
return FALSE
#define LIGHT_CHARGE_COEFF 30000
/obj/item/lightreplacer/proc/Charge(mob/user, coeff)
charge += coeff
if(charge > LIGHT_CHARGE_COEFF)
add_uses(floor(charge / LIGHT_CHARGE_COEFF))
charge = charge % LIGHT_CHARGE_COEFF
#undef LIGHT_CHARGE_COEFF
/obj/item/lightreplacer/proc/replace_light(obj/machinery/light/target, mob/living/user)
//Confirm that it's a light we're testing, because afterattack runs this for everything on a given turf and will runtime
if(!istype(target))
return FALSE
//If the light source is ok then what are we doing here
if(target.status == LIGHT_OK)
user.balloon_alert(user, "light already installed!")
return FALSE
//Were all out
if(!Use(user))
//This balloon alert text is a little redundant, but I want to avoid a new player "yeah i know the light is empty" moment
user.balloon_alert(user, "light replacer empty!")
return FALSE
//remove any broken light on the fixture & add it as a shard
if(target.status != LIGHT_EMPTY)
add_shard(user)
target.status = LIGHT_EMPTY
target.update()
//create a copy of the light type & copy it's params onto the target
var/obj/item/light/old_light = new target.light_type()
target.status = old_light.status
target.switchcount = old_light.switchcount
target.brightness = old_light.brightness
if(obj_flags & EMAGGED)
target.create_reagents(LIGHT_REAGENT_CAPACITY, SEALED_CONTAINER | TRANSPARENT)
target.reagents.add_reagent(/datum/reagent/toxin/plasma, 10)
target.on = target.has_power()
target.update()
//clean up
qdel(old_light)
return TRUE
/obj/item/lightreplacer/advanced
name = "high capacity light replacer"
desc = "A higher capacity light replacer. Refill with broken or working lightbulbs, or sheets of glass."
icon_state = "lightreplacer_high"
max_uses = 50
#define BLIGHTREPLACER_SPOT_COOLDOWN (BLIGHTREPLACER_SPOT_LIFE + 1 SECONDS)
#define BLIGHTREPLACER_SPOT_RANGE 7
#define BLIGHTREPLACER_SPOT_LIFE (5 SECONDS)
/obj/item/lightreplacer/blue
name = "bluespace light replacer"
desc = "A modified light replacer that zaps lights into place. Refill with broken or working lightbulbs, or sheets of glass."
icon_state = "lightreplacer_blue"
bluespace_toggle = TRUE
actions_types = list(/datum/action/item_action/lightreplacer_scan)
action_slots = ALL
custom_materials = list(/datum/material/glass = SHEET_MATERIAL_AMOUNT * 1.5, /datum/material/iron = SHEET_MATERIAL_AMOUNT * 0.75, /datum/material/bluespace = SMALL_MATERIAL_AMOUNT * 3, /datum/material/silver = SMALL_MATERIAL_AMOUNT * 1.5)
COOLDOWN_DECLARE(lightreplacer_spot_cooldown)
/obj/item/lightreplacer/blue/emag_act()
return FALSE // balancing against longrange explosions
/obj/item/lightreplacer/blue/ui_action_click(mob/user, actiontype)
if(!COOLDOWN_FINISHED(src, lightreplacer_spot_cooldown))
balloon_alert(user, "on cooldown!")
return
COOLDOWN_START(src, lightreplacer_spot_cooldown, BLIGHTREPLACER_SPOT_COOLDOWN)
lightreplacer_scan()
/// Scans the area in search of fixtures with broken bulbs in the BLIGHTREPLACER_SPOT_RANGE range, and also marks them with the blue_firefly effect.
/obj/item/lightreplacer/blue/proc/lightreplacer_scan()
var/turf/source_turf = get_turf(src)
for(var/obj/machinery/light/broken_light in dview(BLIGHTREPLACER_SPOT_RANGE, source_turf))
if(broken_light.status == LIGHT_OK)
continue
var/obj/effect/temp_visual/blue_firefly/firefly = new(get_turf(broken_light))
animate(firefly, alpha = 0, time = BLIGHTREPLACER_SPOT_LIFE, easing = CIRCULAR_EASING | EASE_IN)
/datum/action/item_action/lightreplacer_scan
name = "Scan for broken lamps"
desc = "Scans the surrounding area for fixtures with broken light bulbs and marks them."
/obj/effect/temp_visual/blue_firefly
name = "bluespace firefly"
icon = 'icons/effects/effects.dmi'
icon_state = "bluespace_firefly"
light_power = 1
light_range = 2
light_color = LIGHT_COLOR_DARK_BLUE
duration = BLIGHTREPLACER_SPOT_LIFE
/obj/effect/temp_visual/blue_firefly/Initialize(mapload)
. = ..()
update_appearance(UPDATE_OVERLAYS)
/obj/effect/temp_visual/blue_firefly/update_overlays()
. = ..()
. += emissive_appearance(icon, icon_state, src, alpha = src.alpha)
#undef BLIGHTREPLACER_SPOT_COOLDOWN
#undef BLIGHTREPLACER_SPOT_RANGE
#undef BLIGHTREPLACER_SPOT_LIFE
#undef GLASS_SHEET_USES
#undef LIGHTBULB_COST
#undef BULB_SHARDS_REQUIRED