Skip to content

Commit

Permalink
[FEATURE] Decal painter (#27036)
Browse files Browse the repository at this point in the history
* Add_Decal_Painter

* Seperate TGUI file

* Decal painter DmIcon

* Fix some merge errors

* I don't know why this merge is so incredibly fucked

* Update TGUI bundle

* I swear to god

* Fixes DmIcon

* TGUI bundle build

* Remove implicit var

* Final cleanup

* Add decal_painter sprite

* Update code/game/objects/items/devices/painter/decal_painter.dm

Co-authored-by: Luc <[email protected]>
Signed-off-by: Chap <[email protected]>

* Add helper proc for deleting all components of a type from a datum

* Added comments to cycle_style

* Elementized decal fixes

* Use the new decal system

* TGUI bundle

---------

Signed-off-by: Chap <[email protected]>
Co-authored-by: Adrer <[email protected]>
Co-authored-by: Luc <[email protected]>
Co-authored-by: Burzah <[email protected]>
  • Loading branch information
4 people authored Nov 29, 2024
1 parent 3cc903c commit f0ac13b
Show file tree
Hide file tree
Showing 13 changed files with 311 additions and 54 deletions.
2 changes: 2 additions & 0 deletions code/__DEFINES/dcs/basetype_signals.dm
Original file line number Diff line number Diff line change
Expand Up @@ -29,3 +29,5 @@
#define COMSIG_TURF_CHANGE "turf_change"
///from base of turf/proc/onShuttleMove(): (turf/new_turf)
#define COMSIG_TURF_ON_SHUTTLE_MOVE "turf_on_shuttle_move"
///from base of turf/proc/get_decals(): (list/datum/element/decal/decals)
#define COMSIG_ATOM_GET_DECALS "atom_get_decals"
9 changes: 9 additions & 0 deletions code/datums/components/_component.dm
Original file line number Diff line number Diff line change
Expand Up @@ -449,6 +449,15 @@
if(istype(removing, component_to_nuke) && !QDELETED(removing))
qdel(removing)

/**
* Removes all components of a given type from the datum
*/
/datum/proc/DeleteComponentsType(component_type_to_nuke)
var/list/components = GetComponents(component_type_to_nuke)
for(var/datum/component/removing in components)
if(!QDELETED(removing))
qdel(removing)

/**
* Get existing component of type, or create it and return a reference to it
*
Expand Down
4 changes: 2 additions & 2 deletions code/datums/elements/decal_element.dm
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@
if(isitem(target))
INVOKE_ASYNC(target, TYPE_PROC_REF(/obj/item/, update_slot_icon), TRUE)
if(_dir)
RegisterSignal(target, COMSIG_ATOM_DECALS_ROTATING, PROC_REF(shuttle_rotate), TRUE)
RegisterSignal(target, list(COMSIG_ATOM_DECALS_ROTATING, COMSIG_ATOM_GET_DECALS), PROC_REF(get_decals), TRUE)
SSdcs.RegisterSignal(target, COMSIG_ATOM_DIR_CHANGE, TYPE_PROC_REF(/datum/controller/subsystem/processing/dcs, rotate_decals), override=TRUE)
if(_cleanable)
RegisterSignal(target, COMSIG_COMPONENT_CLEAN_ACT, PROC_REF(clean_react), TRUE)
Expand Down Expand Up @@ -172,6 +172,6 @@
Detach(source)
new_turf.AddElement(type, pic.icon, base_icon_state, directional, pic.layer, pic.alpha, pic.color, cleanable, description)

/datum/element/decal/proc/shuttle_rotate(datum/source, list/datum/element/decal/rotating)
/datum/element/decal/proc/get_decals(datum/source, list/datum/element/decal/rotating)
SIGNAL_HANDLER // COMSIG_ATOM_DECALS_ROTATING
rotating += src
4 changes: 2 additions & 2 deletions code/game/objects/effects/decals/turf_decal.dm
Original file line number Diff line number Diff line change
Expand Up @@ -3,11 +3,11 @@
icon_state = "warningline"
layer = TURF_DECAL_LAYER

/obj/effect/turf_decal/Initialize(mapload)
/obj/effect/turf_decal/Initialize(mapload, _dir)
..()
. = INITIALIZE_HINT_QDEL
var/turf/T = loc
if(!istype(T)) //you know this will happen somehow
CRASH("Turf decal initialized in an object/nullspace")

T.AddElement(/datum/element/decal, icon, icon_state, dir, layer, alpha, color, FALSE, null)
T.AddElement(/datum/element/decal, icon, icon_state, _dir || dir, layer, alpha, color, FALSE, null)
3 changes: 0 additions & 3 deletions code/game/objects/effects/decals/turfdecals/markings.dm
Original file line number Diff line number Diff line change
Expand Up @@ -199,9 +199,6 @@
/obj/effect/turf_decal/siding/box
icon_state = "siding_box"

/obj/effect/turf_decal/raven_ship_sign
icon_state = "RAVEN2"

/obj/effect/turf_decal/raven/one
icon_state = "RAVEN1"

Expand Down
5 changes: 5 additions & 0 deletions code/game/objects/effects/decals/turfdecals/weather_decals.dm
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,11 @@
name = "sandy floor"
icon_state = "sandyfloor"

/obj/effect/turf_decal/weather/snow
name = "snowy floor"
icon = 'icons/turf/snow.dmi'
icon_state = "snow"

/obj/effect/turf_decal/weather/snow/corner
name = "snow corner piece"
icon = 'icons/turf/snow.dmi'
Expand Down
117 changes: 117 additions & 0 deletions code/game/objects/items/devices/painter/decal_painter.dm
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
/datum/painter/decal
module_name = "decal painter"
module_state = "decal_painter"
/// icon that contains the decal sprites
var/decal_icon = 'icons/turf/decals.dmi'
/// icon_state of the selected decal
var/decal_state = "warn_box"
var/decal_dir = SOUTH
/// When removal_mode is TRUE the decal painter will remove decals instead
var/removal_mode = FALSE
var/max_decals = 3
var/static/list/decal_blacklist = typecacheof(
list(
/obj/effect/turf_decal/raven,
/obj/effect/turf_decal/weather,
/obj/effect/turf_decal/stripes/asteroid,
/obj/effect/turf_decal/tile,
/obj/effect/turf_decal/sand
)
)
/// Assoc list with icon_state of the decal as the key, and decal path as the value.
var/static/list/lookup_cache_decals = list()

/datum/painter/decal/New(obj/item/painter/parent_painter)
. = ..()
if(!length(lookup_cache_decals))
for(var/D in subtypesof(/obj/effect/turf_decal))
var/obj/effect/turf_decal/decal = D
if(decal in decal_blacklist)
continue
lookup_cache_decals[decal::icon_state] = decal

/datum/painter/decal/paint_atom(atom/target, mob/user)
if(!istype(target, /turf/simulated/floor))
to_chat(user, "<span class='warning'>[holder] can only be used on flooring.</span>")
return FALSE
var/turf/target_turf = get_turf(target)
var/list/datum/element/decal/decals = target_turf.get_decals()
if(removal_mode)
remove_decals(target)
return TRUE
if(length(decals) >= max_decals)
to_chat(user, "<span class='warning'>You can't fit more decals on [target].</span>")
return FALSE
var/typepath = lookup_cache_decals[decal_state]
new typepath(target_turf, decal_dir)
return TRUE

/datum/painter/decal/pick_color(mob/user)
if(!user)
return
ui_interact(user)

/datum/painter/decal/ui_state(mob/user)
return GLOB.inventory_state

/datum/painter/decal/ui_interact(mob/user, datum/tgui/ui = null)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "DecalPainter", module_name)
ui.set_autoupdate(FALSE)
ui.open()

/datum/painter/decal/ui_data(mob/user)
var/list/data = list()
data["selectedStyle"] = decal_state
data["selectedDir"] = decal_dir
data["removalMode"] = removal_mode

return data


/datum/painter/decal/ui_static_data(mob/user)
var/list/data = list()
data["icon"] = decal_icon
data["availableStyles"] = list()
for(var/decal in lookup_cache_decals)
data["availableStyles"] += decal

return data

/datum/painter/decal/ui_act(action, params)
if(..())
return

if(action == "select_style")
var/new_style = params["style"]
if(lookup_cache_decals.Find(new_style) != 0)
decal_state = new_style
removal_mode = FALSE

if(action == "cycle_style") // Cycles through the available styles one at a time
var/index = lookup_cache_decals.Find(decal_state) // Find the index of the currently selected style in the lookup cache
index += params["offset"] // Offset is either -1 or 1. Add this to the index to get the style before or after the current style.
if(index < 1) // If the index is below 1, loop back to the last item in the cache.
index = length(lookup_cache_decals)
if(index > length(lookup_cache_decals)) // If the index is above the length of the cache, loop back to the first item in the cache.
index = 1
decal_state = lookup_cache_decals[index] // Then set our state to the index
removal_mode = FALSE

if(action == "select_direction")
var/dir = params["direction"]
removal_mode = FALSE
if(dir != 0)
decal_dir = dir

if(action == "removal_mode")
removal_mode = !removal_mode
return TRUE

/datum/painter/decal/proc/remove_decals(atom/target)
var/turf/target_turf = get_turf(target)
var/list/datum/element/decal/decals = target_turf.get_decals()
for(var/datum/element/decal/dcl in decals)
dcl.Detach(target)
target_turf.RemoveElement(/datum/element/decal)
6 changes: 4 additions & 2 deletions code/game/objects/items/devices/painter/painter.dm
Original file line number Diff line number Diff line change
Expand Up @@ -12,14 +12,16 @@
"Floor Painter" = /datum/painter/floor,
"Pipe Painter" = /datum/painter/pipe,
"Window Painter" = /datum/painter/pipe/window,
"Airlock Painter" = /datum/painter/airlock)
"Airlock Painter" = /datum/painter/airlock,
"Decal Painter" = /datum/painter/decal)

/// Associative list of painter types, with the value being the icon. (For use in the radial menu)
var/static/list/painter_icon_list = list(
"Floor Painter" = image(icon = 'icons/obj/painting.dmi', icon_state = "floor_painter"),
"Pipe Painter" = image(icon = 'icons/obj/painting.dmi', icon_state = "pipe_painter"),
"Window Painter" = image(icon = 'icons/obj/painting.dmi', icon_state = "window_painter"),
"Airlock Painter" = image(icon = 'icons/obj/painting.dmi', icon_state = "airlock_painter"))
"Airlock Painter" = image(icon = 'icons/obj/painting.dmi', icon_state = "airlock_painter"),
"Decal Painter" = image(icon = 'icons/obj/painting.dmi', icon_state = "decal_painter"))

/// The [/datum/painter] which is currently active.
var/datum/painter/selected_module = null
Expand Down
7 changes: 7 additions & 0 deletions code/game/turfs/turf.dm
Original file line number Diff line number Diff line change
Expand Up @@ -603,6 +603,13 @@
C.take_organ_damage(damage)
C.KnockDown(3 SECONDS)

/// Returns a list of all attached /datum/element/decal/ for this turf
/turf/proc/get_decals()
var/list/datum/element/decals = list()
SEND_SIGNAL(src, COMSIG_ATOM_GET_DECALS, decals)

return decals

/turf/proc/initialize_milla()
var/datum/milla_safe/initialize_turf/milla = new()
milla.invoke_async(src)
Expand Down
Binary file modified icons/obj/painting.dmi
Binary file not shown.
1 change: 1 addition & 0 deletions paradise.dme
Original file line number Diff line number Diff line change
Expand Up @@ -1110,6 +1110,7 @@
#include "code\game\objects\items\devices\voice_changer.dm"
#include "code\game\objects\items\devices\whistle.dm"
#include "code\game\objects\items\devices\painter\airlock_painter.dm"
#include "code\game\objects\items\devices\painter\decal_painter.dm"
#include "code\game\objects\items\devices\painter\floor_painter.dm"
#include "code\game\objects\items\devices\painter\painter.dm"
#include "code\game\objects\items\devices\painter\pipe_painter.dm"
Expand Down
117 changes: 117 additions & 0 deletions tgui/packages/tgui/interfaces/DecalPainter.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,117 @@
import { useBackend, useLocalState } from '../backend';
import { Button, LabeledList, Section, Table, Dropdown, Flex, Icon, Box, DmIcon } from '../components';
import { Window } from '../layouts';

const SelectableTile = (props, context) => {
const { act, data } = useBackend(context);
const { icon_state, direction, isSelected, onSelect } = props;

return (
<DmIcon
icon={data.icon}
icon_state={icon_state}
direction={direction}
onClick={onSelect}
style={{
'border-style': (isSelected && 'solid') || 'none',
'border-width': '2px',
'border-color': 'orange',
padding: (isSelected && '0px') || '2px',
}}
/>
);
};

const Dir = {
NORTH: 1,
SOUTH: 2,
EAST: 4,
WEST: 8,
};

export const DecalPainter = (props, context) => {
const { act, data } = useBackend(context);
const { availableStyles, selectedStyle, selectedDir, removalMode } = data;
return (
<Window width={405} height={475}>
<Window.Content scrollable>
<Section title="Decal setup">
<Flex>
<Flex.Item>
<Button icon="chevron-left" onClick={() => act('cycle_style', { offset: -1 })} />
</Flex.Item>
<Flex.Item>
<Dropdown
options={availableStyles}
selected={selectedStyle}
width="150px"
height="20px"
ml="2px"
mr="2px"
nochevron
onSelected={(val) => act('select_style', { style: val })}
/>
</Flex.Item>
<Flex.Item>
<Button icon="chevron-right" onClick={() => act('cycle_style', { offset: 1 })} />
</Flex.Item>
<Flex.Item>
<Button icon="eraser" color={removalMode ? 'green' : 'transparent'} onClick={() => act('removal_mode')}>
Remove decals
</Button>
</Flex.Item>
</Flex>

<Box mt="5px" mb="5px">
<Flex
overflowY="auto" // scroll
maxHeight="220px" // a bit more than half of all tiles fit in this box at once.
wrap="wrap"
>
{availableStyles.map((style) => (
<Flex.Item key={style}>
<SelectableTile
icon_state={style}
isSelected={selectedStyle === style && !removalMode}
onSelect={() => act('select_style', { style: style })}
/>
</Flex.Item>
))}
</Flex>
</Box>

<LabeledList>
<LabeledList.Item label="Direction">
<Table style={{ display: 'inline' }}>
{[Dir.NORTH, null, Dir.SOUTH].map((latitude) => (
<Table.Row key={latitude}>
{[latitude + Dir.WEST, latitude, latitude + Dir.EAST].map((dir) => (
<Table.Cell
key={dir}
style={{
'vertical-align': 'middle',
'text-align': 'center',
}}
>
{dir === null ? (
<Icon name="arrows-alt" size={3} />
) : (
<SelectableTile
icon_state={selectedStyle}
direction={dir}
isSelected={dir === selectedDir && !removalMode}
onSelect={() => act('select_direction', { direction: dir })}
/>
)}
</Table.Cell>
))}
</Table.Row>
))}
</Table>
</LabeledList.Item>
</LabeledList>
</Section>
</Window.Content>
</Window>
);
};
90 changes: 45 additions & 45 deletions tgui/public/tgui.bundle.js

Large diffs are not rendered by default.

0 comments on commit f0ac13b

Please sign in to comment.