Makes it possible to add new mech construction steps without ruining everything (#94175)

## About The Pull Request

Currently mech construction icons correspond 1:1 with construction step

<img width="1357" height="349" alt="image"
src="https://github.com/user-attachments/assets/65763f51-776c-44a7-bc9e-b710db9342a1"
/>

This means if you want to add a new step in the middle of an existing
construction chain you are SOL and have to update everything manually

This suuuucks, and you will notice in the picture above that many states
*are identical*

So I have lightly refactored it:

- When steps are instantiated, it will automatically attempt to fill in
`icon_state`s according to the index in the list, as it does currently
- If the step specifies its own icon state, nothing happens, but index
goes up
- If the step specifies `skip_state`, it will neither set an icon state
nor increment the index

This means you can insert a step in the middle of an existing chain with
`skip_state = TRUE` if your state has no icon associated, OR you can
insert a step with `icon_state = "new_state", skip_state = TRUE` to add
your icon without needing to edit every existing icon

Now in an ideal world we get rid of this auto-setting system wholesale
and set bespoke icon states (`mecha_wires`, `mecha_internal_armor`,
etc). We would just define a step's `state = "wired"`, `state =
"armored"`, and so on. However, I feel like having the option of using
"default states" makes it easier during development.

Also I *could* go through and remove all the duplicate states and
replace them with `skip_state` instructions but I'm lazy. (Maybe I'll do
it anyways though)

Other changes

- Completing a construction step sends you a chat message about the next
step, so you don't have to examine the chassis to find out
- Changed around how the Phazon accepts its anomaly core

## Changelog

🆑 Melbert
refactor: Refactored how mech icons update mid-construction, report any
disappearing mechs please
fix: One of the Phazon's mid construction icons was not visible in the
past, now it is again. Yippee.
/🆑
This commit is contained in:
MrMelbert
2025-12-02 16:50:43 -07:00
committed by GitHub
parent 471c70dfe1
commit 0098ffca01
5 changed files with 151 additions and 100 deletions
+1
View File
@@ -214,6 +214,7 @@
#include "mapping.dm"
#include "mapping_nearstation_test.dm"
#include "market.dm"
#include "mecha_build.dm"
#include "mecha_damage.dm"
#include "medical_wounds.dm"
#include "merge_type.dm"
+39
View File
@@ -0,0 +1,39 @@
/datum/unit_test/mecha_construction_icons
/datum/unit_test/mecha_construction_icons/Run()
for(var/chassis_type in subtypesof(/obj/item/mecha_parts/chassis))
var/obj/item/mecha_parts/chassis/chassis = allocate(chassis_type)
var/datum/component/construction/unordered/mecha_chassis/chassis_comp = chassis.GetComponent(/datum/component/construction/unordered/mecha_chassis)
if(isnull(chassis_comp))
TEST_FAIL("[chassis_type]: Mecha chassis without a construction component")
continue
chassis_comp.spawn_result()
var/datum/component/construction/mecha/construction_comp = chassis.GetComponent(/datum/component/construction/mecha)
if(isnull(construction_comp))
TEST_FAIL("[chassis_type]: Finished chassis without a mech construction component")
continue
if(!QDELETED(chassis_comp))
TEST_FAIL("[chassis_type]: Chassis construction component was not deleted after applying the mecha construction component")
continue
if(isnull(construction_comp.base_icon))
continue // apparently valid, for construction which don't have *any* icon states
var/list/all_chassis_icon_states = icon_states_fast(chassis.icon)
var/list/step_icon_states = list()
for(var/list/step_data as anything in construction_comp.steps)
var/icon_state = step_data["icon_state"]
if(isnull(icon_state))
continue // valid, it just means the step doesn't change the icon
if(!(icon_state in all_chassis_icon_states))
TEST_FAIL("[chassis_type]: Mecha construction step has invalid icon_state '[icon_state]'")
continue
step_icon_states += icon_state
for(var/icon_state in all_chassis_icon_states - step_icon_states)
// little extra logic here to avoid false positives like finding "mech" in "darkmech"
if(!findtext("test-[icon_state]", "test-[construction_comp.base_icon]"))
continue
TEST_FAIL("[chassis_type]: Mecha construction has an unused icon state '[icon_state]'")
@@ -1,7 +1,4 @@
////////////////////////////////
///// Construction datums //////
////////////////////////////////
/// Mecha construction
/datum/component/construction/mecha
var/base_icon
@@ -42,17 +39,35 @@
// Override if the mech needs an entirely custom process (See HONK mech)
// Otherwise override specific steps as needed (Ripley, Clarke, Phazon)
/datum/component/construction/mecha/proc/get_steps()
return get_frame_steps() + get_circuit_steps() + (circuit_weapon ? get_circuit_weapon_steps() : list()) + get_stockpart_steps() + get_inner_plating_steps() + get_outer_plating_steps()
var/list/all_steps = \
get_frame_steps() + \
get_circuit_steps() + \
get_circuit_weapon_steps() + \
get_stockpart_steps() + \
get_inner_plating_steps() + \
get_outer_plating_steps()
/datum/component/construction/mecha/update_parent(step_index)
steps = get_steps()
..()
// By default, each step in mech construction has a single icon_state:
// "[base_icon][index - 1]"
// For example, Ripley's step 1 icon_state is "ripley0"
var/atom/parent_atom = parent
if(!steps[index]["icon_state"] && base_icon)
parent_atom.icon_state = "[base_icon][index - 1]"
// If you don't set a construction icon state, one will automatically be assigned
// based on the index of the step in the step list
//
// If you do set a custom icon state, it will not be overridden, but the state will still increment.
//
// You can use skip_state to prevent a step from increasing the index,
// useful for steps which don't affect the mech's appearance at all
// or for steps which have custom icon states that don't follow the normal pattern.
var/state = 0
for(var/list/step_data as anything in all_steps)
if(step_data["skip_state"])
continue
step_data["icon_state"] ||= "[base_icon][state]"
state += 1
return all_steps
/datum/component/construction/mecha/Initialize()
steps ||= get_steps()
return ..()
/datum/component/construction/unordered/mecha_chassis/custom_action(obj/item/I, mob/living/user, typepath)
. = user.transferItemToLoc(I, parent)
@@ -67,7 +82,7 @@
parent_atom.icon = 'icons/mob/rideables/mech_construction.dmi'
parent_atom.set_density(TRUE)
parent_atom.cut_overlays()
..()
return ..()
// Default proc for the first steps of mech construction.
/datum/component/construction/mecha/proc/get_frame_steps()
@@ -82,7 +97,7 @@
"back_key" = TOOL_WRENCH,
"desc" = "The hydraulic systems are connected, and can be activated with a <b>screwdriver</b>.",
"forward_message" = "activated the hydraulic systems",
"backward_message" = "disconnected the hydraulic systems"
"backward_message" = "disconnected the hydraulic systems",
),
list(
"key" = /obj/item/stack/cable_coil,
@@ -90,14 +105,14 @@
"back_key" = TOOL_SCREWDRIVER,
"desc" = "The hydraulic systems are active, and the frame can be <b>wired</b>.",
"forward_message" = "added wiring",
"backward_message" = "deactivated the hydraulic systems"
"backward_message" = "deactivated the hydraulic systems",
),
list(
"key" = TOOL_WIRECUTTER,
"back_key" = TOOL_SCREWDRIVER,
"desc" = "The wiring is added, and can be adjusted with <b>wirecutters</b>.",
"forward_message" = "adjusted wiring",
"backward_message" = "removed wiring"
"backward_message" = "removed wiring",
)
)
@@ -111,14 +126,14 @@
"back_key" = TOOL_SCREWDRIVER,
"desc" = "The wiring is adjusted, and the <b>central control module</b> slot has opened.",
"forward_message" = "added central control module",
"backward_message" = "disconnected wiring"
"backward_message" = "disconnected wiring",
),
list(
"key" = TOOL_SCREWDRIVER,
"back_key" = TOOL_CROWBAR,
"desc" = "Central control module is installed, and can be <b>screwed</b> into place.",
"forward_message" = "secured central control module",
"backward_message" = "removed central control module"
"backward_message" = "removed central control module",
),
list(
"key" = circuit_periph,
@@ -126,38 +141,42 @@
"back_key" = TOOL_SCREWDRIVER,
"desc" = "Central control module is secured, and the <b>peripheral control module</b> slot has opened.",
"forward_message" = "added peripheral control module",
"backward_message" = "unsecured central control module"
"backward_message" = "unsecured central control module",
),
list(
"key" = TOOL_SCREWDRIVER,
"back_key" = TOOL_CROWBAR,
"desc" = "Peripheral control module is installed, and can be <b>screwed</b> into place.",
"forward_message" = "secured peripheral control module",
"backward_message" = "removed peripheral control module"
"backward_message" = "removed peripheral control module",
)
)
// Default proc for weapon circuitboard steps
// Used by combat mechs
/datum/component/construction/mecha/proc/get_circuit_weapon_steps()
if(!circuit_weapon)
return list()
return list(
list(
"key" = circuit_weapon,
"action" = ITEM_DELETE,
"back_key" = TOOL_SCREWDRIVER,
"desc" = "Peripherals control module is secured, and the <b>weapon control module<b> slot has opened.",
"desc" = "Peripherals control module is secured, and the <b>weapon control module</b> slot has opened.",
"forward_message" = "added weapon control module",
"backward_message" = "unsecured peripheral control module"
"backward_message" = "unsecured peripheral control module",
),
list(
"key" = TOOL_SCREWDRIVER,
"back_key" = TOOL_CROWBAR,
"desc" = "Weapon control module is installed, and can be <b>screwed</b> into place.",
"forward_message" = "secured weapon control module",
"backward_message" = "removed weapon control module"
"backward_message" = "removed weapon control module",
)
)
// Default proc for stock part installation
// Third set of steps by default
/datum/component/construction/mecha/proc/get_stockpart_steps()
@@ -171,14 +190,14 @@
"back_key" = TOOL_SCREWDRIVER,
"desc" = prevstep_text,
"forward_message" = "added scanning module",
"backward_message" = backward_text
"backward_message" = backward_text,
),
list(
"key" = TOOL_SCREWDRIVER,
"back_key" = TOOL_CROWBAR,
"desc" = "Scanning module is installed, and can be <b>screwed</b> into place.",
"forward_message" = "secured scanning module",
"backward_message" = "removed scanning module"
"backward_message" = "removed scanning module",
),
list(
"key" = /obj/item/stock_parts/capacitor,
@@ -186,14 +205,14 @@
"back_key" = TOOL_SCREWDRIVER,
"desc" = "Scanning module is secured, the <b>capacitor</b> can be added.",
"forward_message" = "added capacitor",
"backward_message" = "unscecured scanning module"
"backward_message" = "unscecured scanning module",
),
list(
"key" = TOOL_SCREWDRIVER,
"back_key" = TOOL_CROWBAR,
"desc" = "Capacitor is installed, and can be <b>screwed</b> into place.",
"forward_message" = "secured capacitor",
"backward_message" = "removed capacitor"
"backward_message" = "removed capacitor",
),
list(
"key" = /obj/item/stock_parts/servo,
@@ -201,14 +220,14 @@
"back_key" = TOOL_SCREWDRIVER,
"desc" = "Scanning module is secured, the <b>servo</b> can be added.",
"forward_message" = "added servo",
"backward_message" = "unsecured capacitor"
"backward_message" = "unsecured capacitor",
),
list(
"key" = TOOL_SCREWDRIVER,
"back_key" = TOOL_CROWBAR,
"desc" = "Servo is installed, and can be <b>screwed</b> into place.",
"forward_message" = "secured servo",
"backward_message" = "removed servo"
"backward_message" = "removed servo",
),
list(
"key" = /obj/item/stock_parts/power_store/cell,
@@ -216,14 +235,14 @@
"back_key" = TOOL_SCREWDRIVER,
"desc" = "Servo is secured, and the <b>power cell</b> can be added.",
"forward_message" = "added power cell",
"backward_message" = "unsecured servo"
"backward_message" = "unsecured servo",
),
list(
"key" = TOOL_SCREWDRIVER,
"back_key" = TOOL_CROWBAR,
"desc" = "The power cell is installed, and can be <b>screwed</b> into place.",
"forward_message" = "secured power cell",
"backward_message" = "removed power cell"
"backward_message" = "removed power cell",
)
)
@@ -239,7 +258,7 @@
"back_key" = TOOL_SCREWDRIVER,
"desc" = "The power cell is secured, [inner_plating_amount] sheets of [initial(inner_plating.name)] can be used as inner plating.",
"forward_message" = "installed internal armor layer",
"backward_message" = "unsecured power cell"
"backward_message" = "unsecured power cell",
)
)
else
@@ -250,7 +269,7 @@
"back_key" = TOOL_SCREWDRIVER,
"desc" = "The power cell is secured, [initial(inner_plating.name)] can be used as inner plating.",
"forward_message" = "installed internal armor layer",
"backward_message" = "unsecured power cell"
"backward_message" = "unsecured power cell",
)
)
@@ -260,14 +279,14 @@
"back_key" = TOOL_CROWBAR,
"desc" = "Inner plating is installed, and can be <b>wrenched</b> into place.",
"forward_message" = "secured internal armor layer",
"backward_message" = "pried off internal armor layer"
"backward_message" = "pried off internal armor layer",
),
list(
"key" = TOOL_WELDER,
"back_key" = TOOL_WRENCH,
"desc" = "Inner plating is wrenched, and can be <b>welded</b>.",
"forward_message" = "welded internal armor layer",
"backward_message" = "unfastened internal armor layer"
"backward_message" = "unfastened internal armor layer",
)
)
@@ -283,7 +302,7 @@
"back_key" = TOOL_WELDER,
"desc" = "Inner plating is welded, [outer_plating_amount] sheets of [initial(outer_plating.name)] can be used as external armor.",
"forward_message" = "installed external armor layer",
"backward_message" = "cut off internal armor layer"
"backward_message" = "cut off internal armor layer",
)
)
else
@@ -294,7 +313,7 @@
"back_key" = TOOL_WELDER,
"desc" = "Inner plating is welded, [initial(outer_plating.name)] can be used as external armor.",
"forward_message" = "installed external armor layer",
"backward_message" = "cut off internal armor layer"
"backward_message" = "cut off internal armor layer",
)
)
@@ -304,14 +323,14 @@
"back_key" = TOOL_CROWBAR,
"desc" = "External armor is installed, and can be <b>wrenched</b> into place.",
"forward_message" = "secured external armor layer",
"backward_message" = "pried off external armor layer"
"backward_message" = "pried off external armor layer",
),
list(
"key" = TOOL_WELDER,
"back_key" = TOOL_WRENCH,
"desc" = "External armor is wrenched, and can be <b>welded</b>.",
"forward_message" = "welded external armor layer",
"backward_message" = "unfastened external armor layer"
"backward_message" = "unfastened external armor layer",
)
)
@@ -322,8 +341,15 @@
if(diff == FORWARD && steps[index]["forward_message"])
user.balloon_alert_to_viewers(steps[index]["forward_message"])
var/list/next_step = index + 1 == steps.len ? null : steps[index + 1]
if(next_step?["desc"])
to_chat(user, span_smallnoticeital(next_step["desc"]))
else if(steps[index]["backward_message"])
user.balloon_alert_to_viewers(steps[index]["backward_message"])
var/list/last_step = index <= 1 ? null : steps[index - 1]
if(last_step?["desc"])
to_chat(user, span_smallnoticeital(last_step["desc"]))
return TRUE
@@ -352,22 +378,22 @@
outer_plating_amount = 10
/datum/component/construction/mecha/ripley/get_outer_plating_steps()
// we yoink the first step of adding plating and modify the flavor a bit
var/list/first_step = ..()[1]
first_step["desc"] = "Plating is welded, and 10 <b>rods</b> can be used to install the cockpit."
first_step["forward_message"] = "installed cockpit"
first_step["backward_message"] = "cut off plating"
// then we add our own second step for welding the cockpit in place
return list(
list(
"key" = /obj/item/stack/rods,
"amount" = 10,
"back_key" = TOOL_WELDER,
"desc" = "Outer plating is welded, and 10 <b>rods</b> can be used to install the cockpit.",
"forward_message" = "installed cockpit",
"backward_message" = "cut off outer armor layer"
),
first_step,
list(
"key" = TOOL_WELDER,
"back_key" = TOOL_WIRECUTTER,
"desc" = "Cockpit wire screen is installed, and can be <b>welded</b>.",
"forward_message" = "welded cockpit",
"backward_message" = "cut off cockpit"
),
"backward_message" = "cut off cockpit",
)
)
//GYGAX
@@ -584,7 +610,7 @@
var/atom/parent_atom = parent
parent_atom.icon = 'icons/mob/rideables/mech_construct.dmi'
parent_atom.icon_state = "honker_chassis"
..()
return ..()
/datum/component/construction/mecha/honker/custom_action(obj/item/I, mob/living/user, diff)
if(istype(I, /obj/item/bikehorn))
@@ -646,6 +672,18 @@
outer_plating = /obj/item/mecha_parts/part/phazon_armor
outer_plating_amount = 1
var/obj/item/required_core = /obj/item/assembly/signaler/anomaly/ectoplasm
/datum/component/construction/mecha/phazon/custom_action(obj/item/I, mob/living/user, diff)
if(!..())
return FALSE
if(istype(I, /obj/item/assembly/signaler/anomaly) && !istype(I, required_core))
to_chat(user, span_warning("The anomaly core socket only accepts \a [initial(required_core.name)]!"))
return FALSE
return TRUE
/datum/component/construction/mecha/phazon/get_stockpart_steps()
return list(
list(
@@ -654,14 +692,14 @@
"back_key" = TOOL_SCREWDRIVER,
"desc" = "Weapon control module is secured, and the <b>scanning module</b> can be added.",
"forward_message" = "added scanning module",
"backward_message" = "unsecured weapon control module"
"backward_message" = "unsecured weapon control module",
),
list(
"key" = TOOL_SCREWDRIVER,
"back_key" = TOOL_CROWBAR,
"desc" = "Scanning module is installed, and can be <b>screwed</b> into place.",
"forward_message" = "secured scanning module",
"backward_message" = "removed scanning module"
"backward_message" = "removed scanning module",
),
list(
"key" = /obj/item/stock_parts/capacitor,
@@ -669,14 +707,14 @@
"back_key" = TOOL_SCREWDRIVER,
"desc" = "Scanning module is secured, and the <b>capacitor</b> can be added.",
"forward_message" = "added capacitor",
"backward_message" = "unsecured scanning module"
"backward_message" = "unsecured scanning module",
),
list(
"key" = TOOL_SCREWDRIVER,
"back_key" = TOOL_CROWBAR,
"desc" = "Capacitor is installed, and can be <b>screwed</b> into place.",
"forward_message" = "secured capacitor",
"backward_message" = "removed capacitor"
"backward_message" = "removed capacitor",
),
list(
"key" = /obj/item/stock_parts/servo,
@@ -684,14 +722,14 @@
"back_key" = TOOL_SCREWDRIVER,
"desc" = "Capacitor is secured, the <b>servo</b> can be added.",
"forward_message" = "added servo",
"backward_message" = "unsecured capacitor"
"backward_message" = "unsecured capacitor",
),
list(
"key" = TOOL_SCREWDRIVER,
"back_key" = TOOL_CROWBAR,
"desc" = "Servo is installed, and can be <b>screwed</b> into place.",
"forward_message" = "secured servo",
"backward_message" = "removed servo"
"backward_message" = "removed servo",
),
list(
"key" = /obj/item/stack/ore/bluespace_crystal,
@@ -699,7 +737,7 @@
"back_key" = TOOL_SCREWDRIVER,
"desc" = "Servo is secured, and the <b>bluespace crystal</b> can be added.",
"forward_message" = "added bluespace crystal",
"backward_message" = "unsecured servo"
"backward_message" = "unsecured servo",
),
list(
"key" = /obj/item/stack/cable_coil,
@@ -707,14 +745,16 @@
"back_key" = TOOL_CROWBAR,
"desc" = "The bluespace crystal is installed, and can be <b>wired</b> to the mech systems.",
"forward_message" = "connected bluespace crystal",
"backward_message" = "removed bluespace crystal"
"backward_message" = "removed bluespace crystal",
"icon_state" = "phazon19",
),
list(
"key" = TOOL_SCREWDRIVER,
"back_key" = TOOL_WIRECUTTER,
"desc" = "The bluespace crystal is connected, and the system can be engaged with a <b>screwdriver</b>.",
"forward_message" = "engaded bluespace crystal",
"backward_message" = "disconnected bluespace crystal"
"backward_message" = "disconnected bluespace crystal",
"icon_state" = "phazon20",
),
list(
"key" = /obj/item/stock_parts/power_store/cell,
@@ -722,52 +762,29 @@
"back_key" = TOOL_SCREWDRIVER,
"desc" = "The bluespace crystal is engaged, and the <b>power cell</b> can be added.",
"forward_message" = "added power cell",
"backward_message" = "disengaged bluespace crystal"
"backward_message" = "disengaged bluespace crystal",
"icon_state" = "phazon21",
),
list(
"key" = TOOL_SCREWDRIVER,
"back_key" = TOOL_CROWBAR,
"desc" = "The power cell is installed, and can be <b>screwed</b> into place.",,
"desc" = "The power cell is installed, and can be <b>screwed</b> into place.",
"forward_message" = "secured power cell",
"backward_message" = "removed power cell",
"icon_state" = "phazon19"
// This is the point where a step icon is skipped, so "icon_state" had to be set manually starting from here.
"icon_state" = "phazon21",
)
)
/datum/component/construction/mecha/phazon/get_outer_plating_steps()
return list(
return ..() + list(
list(
"key" = outer_plating,
"amount" = 1,
"key" = required_core,
"action" = ITEM_DELETE,
"back_key" = TOOL_WELDER,
"desc" = "Internal armor is welded, [initial(outer_plating.name)] can be used as external armor.",
"forward_message" = "added external armor layer",
"backward_message" = "cut off internal armor layer"
),
list(
"key" = TOOL_WRENCH,
"back_key" = TOOL_CROWBAR,
"desc" = "External armor is installed, and can be <b>wrenched</b> into place.",
"forward_message" = "secured external armor layer",
"backward_message" = "pried off external armor"
),
list(
"key" = TOOL_WELDER,
"back_key" = TOOL_WRENCH,
"desc" = "External armor is wrenched, and can be <b>welded</b>.",
"forward_message" = "welded external armor",
"backward_message" = "unfastened external armor layer"
),
list(
"key" = /obj/item/assembly/signaler/anomaly/ectoplasm,
"action" = ITEM_DELETE,
"back_key" = TOOL_WELDER,
"desc" = "The external armor is welded, and the <b>ectoplasm anomaly core</b> socket is open.",
"icon_state" = "phazon26",
"forward_message" = "inserted ectoplasm anomaly core",
"backward_message" = "cut off external armor"
"desc" = "The external armor is welded, and the <b>[initial(required_core.name)]</b> socket is open.",
"forward_message" = "inserted [initial(required_core.name)]",
"backward_message" = "cut off external armor",
"skip_state" = TRUE,
)
)
@@ -255,12 +255,6 @@
name = "\improper Phazon chassis"
construct_type = /datum/component/construction/unordered/mecha_chassis/phazon
/obj/item/mecha_parts/chassis/phazon/item_interaction(mob/living/user, obj/item/tool, list/modifiers)
if(istype(tool, /obj/item/assembly/signaler/anomaly) && !istype(tool, /obj/item/assembly/signaler/anomaly/ectoplasm))
to_chat(user, "The anomaly core socket only accepts ectoplasm anomaly cores!")
return ITEM_INTERACT_BLOCKING
return ..()
/obj/item/mecha_parts/part/phazon_torso
name="\improper Phazon torso"
desc="A Phazon torso part. The socket for the ectoplasmic core that powers the exosuit's unique phase drives is located in the middle."
Binary file not shown.

Before

Width:  |  Height:  |  Size: 86 KiB

After

Width:  |  Height:  |  Size: 90 KiB