diff --git a/_maps/map_files/BoxStation/BoxStation.dmm b/_maps/map_files/BoxStation/BoxStation.dmm index a6b4289fe7..d9d8bd243c 100644 --- a/_maps/map_files/BoxStation/BoxStation.dmm +++ b/_maps/map_files/BoxStation/BoxStation.dmm @@ -38756,12 +38756,12 @@ /turf/open/floor/engine/vacuum, /area/maintenance/disposal/incinerator) "coq" = ( -/obj/machinery/atmospherics/components/unary/vent_pump/siphon/atmos/incinerator_output{ - dir = 1 - }, /obj/structure/sign/warning/vacuum/external{ pixel_y = -32 }, +/obj/machinery/atmospherics/components/unary/vent_scrubber/on{ + dir = 1 + }, /turf/open/floor/engine/vacuum, /area/maintenance/disposal/incinerator) "cor" = ( diff --git a/_maps/map_files/BoxedInStation/BoxedInStation.dmm b/_maps/map_files/BoxedInStation/BoxedInStation.dmm index ef3fdc2884..bbf5b18f96 100644 --- a/_maps/map_files/BoxedInStation/BoxedInStation.dmm +++ b/_maps/map_files/BoxedInStation/BoxedInStation.dmm @@ -18372,12 +18372,12 @@ /turf/open/floor/carpet, /area/service/chapel/main) "jxP" = ( -/obj/machinery/atmospherics/components/unary/vent_pump/siphon/atmos/incinerator_output{ - dir = 1 - }, /obj/structure/sign/warning/vacuum/external{ pixel_y = -32 }, +/obj/machinery/atmospherics/components/unary/vent_scrubber/on{ + dir = 1 + }, /turf/open/floor/engine/vacuum, /area/maintenance/disposal/incinerator) "jxW" = ( diff --git a/_maps/map_files/CogStation/CogStation.dmm b/_maps/map_files/CogStation/CogStation.dmm index f85f83092b..e53ca872a2 100644 --- a/_maps/map_files/CogStation/CogStation.dmm +++ b/_maps/map_files/CogStation/CogStation.dmm @@ -65390,7 +65390,7 @@ /turf/open/floor/plasteel, /area/engineering/atmos) "cLY" = ( -/obj/machinery/atmospherics/components/unary/vent_pump/siphon/atmos/incinerator_output{ +/obj/machinery/atmospherics/components/unary/vent_scrubber/on{ dir = 4 }, /turf/open/floor/engine/vacuum, diff --git a/_maps/map_files/KiloStation/KiloStation.dmm b/_maps/map_files/KiloStation/KiloStation.dmm index 1d316f8ad5..3520c115fb 100644 --- a/_maps/map_files/KiloStation/KiloStation.dmm +++ b/_maps/map_files/KiloStation/KiloStation.dmm @@ -18916,9 +18916,6 @@ /obj/effect/mapping_helpers/airlock/cyclelink_helper{ dir = 1 }, -/obj/machinery/atmospherics/pipe/simple/general/visible{ - dir = 6 - }, /turf/open/floor/engine, /area/maintenance/disposal/incinerator) "aFW" = ( @@ -18943,9 +18940,6 @@ /obj/structure/cable{ icon_state = "1-2" }, -/obj/machinery/atmospherics/pipe/simple/general/visible{ - dir = 5 - }, /obj/machinery/igniter{ id = "Incinerator" }, @@ -77085,7 +77079,7 @@ /area/maintenance/port/fore) "cwH" = ( /obj/machinery/atmospherics/components/unary/vent_scrubber/on{ - dir = 8 + dir = 1 }, /turf/open/floor/engine/vacuum, /area/maintenance/disposal/incinerator) @@ -84416,9 +84410,7 @@ /turf/open/floor/plasteel/showroomfloor, /area/security/warden) "jHJ" = ( -/obj/machinery/atmospherics/pipe/simple/general/visible{ - dir = 9 - }, +/obj/item/pipe/directional, /turf/closed/wall/r_wall, /area/maintenance/disposal/incinerator) "jHS" = ( diff --git a/_maps/map_files/MetaStation/MetaStation.dmm b/_maps/map_files/MetaStation/MetaStation.dmm index d96866553e..c385def3b9 100644 --- a/_maps/map_files/MetaStation/MetaStation.dmm +++ b/_maps/map_files/MetaStation/MetaStation.dmm @@ -29553,7 +29553,7 @@ /turf/open/floor/plating, /area/maintenance/starboard/aft) "cnd" = ( -/obj/machinery/atmospherics/components/unary/vent_pump/siphon/atmos/incinerator_output{ +/obj/machinery/atmospherics/components/unary/vent_scrubber/on{ dir = 1 }, /turf/open/floor/engine/vacuum, @@ -46923,7 +46923,7 @@ /turf/open/floor/plating, /area/maintenance/port/fore) "dox" = ( -/obj/machinery/atmospherics/components/unary/vent_pump/on{ +/obj/machinery/atmospherics/components/unary/vent_scrubber/on{ dir = 1 }, /turf/open/floor/engine/vacuum, diff --git a/_maps/map_files/OmegaStation/OmegaStation.dmm b/_maps/map_files/OmegaStation/OmegaStation.dmm index 0d44c8f23b..6c8762697f 100644 --- a/_maps/map_files/OmegaStation/OmegaStation.dmm +++ b/_maps/map_files/OmegaStation/OmegaStation.dmm @@ -36226,7 +36226,7 @@ /turf/open/floor/plasteel, /area/engineering/atmos) "iye" = ( -/obj/machinery/atmospherics/components/unary/vent_pump/siphon/atmos/incinerator_output{ +/obj/machinery/atmospherics/components/unary/vent_scrubber/on{ dir = 4 }, /turf/open/floor/engine/vacuum, diff --git a/_maps/map_files/PubbyStation/PubbyStation.dmm b/_maps/map_files/PubbyStation/PubbyStation.dmm index 4c4d336cb2..3977c8d609 100644 --- a/_maps/map_files/PubbyStation/PubbyStation.dmm +++ b/_maps/map_files/PubbyStation/PubbyStation.dmm @@ -44454,7 +44454,7 @@ /turf/open/floor/engine, /area/maintenance/disposal/incinerator) "cbF" = ( -/obj/machinery/atmospherics/components/unary/vent_pump/siphon/atmos/incinerator_output{ +/obj/machinery/atmospherics/components/unary/vent_scrubber/on{ dir = 8 }, /turf/open/floor/engine, diff --git a/_maps/map_files/TauStation/TauStation.dmm b/_maps/map_files/TauStation/TauStation.dmm index 1d64701453..46753c2ed8 100644 --- a/_maps/map_files/TauStation/TauStation.dmm +++ b/_maps/map_files/TauStation/TauStation.dmm @@ -13544,12 +13544,12 @@ /turf/open/floor/plasteel, /area/commons/fitness/recreation) "eJe" = ( -/obj/machinery/atmospherics/components/unary/vent_pump/siphon/atmos/incinerator_output{ - dir = 1 - }, /obj/structure/sign/warning/vacuum/external{ pixel_y = -32 }, +/obj/machinery/atmospherics/components/unary/vent_scrubber/on{ + dir = 1 + }, /turf/open/floor/engine/vacuum, /area/maintenance/disposal/incinerator) "eJh" = ( diff --git a/code/__DEFINES/tgs.dm b/code/__DEFINES/tgs.dm index e35ccb69d1..c8ef71112b 100644 --- a/code/__DEFINES/tgs.dm +++ b/code/__DEFINES/tgs.dm @@ -1,6 +1,6 @@ // tgstation-server DMAPI -#define TGS_DMAPI_VERSION "6.3.0" +#define TGS_DMAPI_VERSION "6.4.2" // All functions and datums outside this document are subject to change with any version and should not be relied on. diff --git a/code/__SPLURTCODE/DEFINES/traits.dm b/code/__SPLURTCODE/DEFINES/traits.dm index 7b4d3ffed1..c429752e3d 100644 --- a/code/__SPLURTCODE/DEFINES/traits.dm +++ b/code/__SPLURTCODE/DEFINES/traits.dm @@ -42,3 +42,4 @@ #define TRAIT_DUMB_CUM "dumb_for_cum_base" #define TRAIT_DUMB_CUM_CRAVE "dumb_for_cum_need" #define TRAIT_RAD_FIEND "RadFiend" +#define TRAIT_BODY_MORPHER "body_morpher" diff --git a/code/_onclick/click.dm b/code/_onclick/click.dm index b3a75c024c..9a1e2cc514 100644 --- a/code/_onclick/click.dm +++ b/code/_onclick/click.dm @@ -54,7 +54,7 @@ var/list/modifiers = params2list(params) if(modifiers["shift"] && modifiers["middle"]) - return ShiftMiddleClickOn(A) + return ShiftMiddleClickOn(A, params) if(modifiers["shift"] && modifiers["ctrl"]) return CtrlShiftClickOn(A) if(modifiers["middle"]) @@ -402,8 +402,8 @@ return A.CtrlShiftClick(src) -/mob/proc/ShiftMiddleClickOn(atom/A) - src.pointed(A) +/mob/proc/ShiftMiddleClickOn(atom/A, params) + pointed(A, params) return /atom/proc/CtrlShiftClick(mob/user) diff --git a/code/game/machinery/doors/door.dm b/code/game/machinery/doors/door.dm index d565984bf0..a3483e5702 100644 --- a/code/game/machinery/doors/door.dm +++ b/code/game/machinery/doors/door.dm @@ -44,6 +44,8 @@ var/red_alert_access = FALSE //if TRUE, this door will always open on red alert var/poddoor = FALSE var/unres_sides = 0 //Unrestricted sides. A bitflag for which direction (if any) can open the door with no access + /// Whether or not the door can be opened by hand (used for blast doors and shutters) + var/can_open_with_hands = TRUE /obj/machinery/door/examine(mob/user) . = ..() @@ -55,10 +57,16 @@ if(!poddoor) . += "Its maintenance panel is screwed in place." -/obj/machinery/door/add_context(atom/source, list/context, obj/item/held_item, mob/living/user) +/obj/machinery/door/add_context(atom/source, list/context, obj/item/held_item, mob/user) . = ..() - if (isnull(held_item) && !istype(src, /obj/machinery/door/firedoor)) // You cannot open/close with your hands + if(!can_open_with_hands) + return . + + if(isaicamera(user) || issilicon(user)) + return . + + if(isnull(held_item) && Adjacent(user)) LAZYSET(context[SCREENTIP_CONTEXT_LMB], INTENT_ANY, (density ? "Open" : "Close")) return CONTEXTUAL_SCREENTIP_SET @@ -100,7 +108,7 @@ return ..() /obj/machinery/door/Bumped(atom/movable/AM) - if(operating || (obj_flags & EMAGGED)) + if(operating || (obj_flags & EMAGGED) || (!can_open_with_hands && density)) return if(ismob(AM)) var/mob/B = AM @@ -132,9 +140,9 @@ return !opacity /obj/machinery/door/proc/bumpopen(mob/user) - if(operating) + if(operating || !can_open_with_hands) return - src.add_fingerprint(user) + add_fingerprint(user) if(!src.requiresID()) user = null @@ -155,7 +163,7 @@ /obj/machinery/door/proc/try_to_activate_door(mob/user) add_fingerprint(user) - if(operating || (obj_flags & EMAGGED)) + if(operating || (obj_flags & EMAGGED) || !can_open_with_hands) return if(!requiresID()) user = null //so allowed(user) always succeeds @@ -417,3 +425,14 @@ . = ..() if(!density) return . * EXPLOSION_DAMAGE_OPEN_DOOR_FACTOR + +/obj/machinery/door/shuttleRotate(rotation, params) + . = ..() + if(!unres_sides) + return + var/new_unres_sides + for(var/direction in GLOB.cardinals) + if(unres_sides & direction) + new_unres_sides |= angle2dir_cardinal(rotation+dir2angle(direction)) + unres_sides = new_unres_sides + update_icon() diff --git a/code/game/machinery/doors/firedoor.dm b/code/game/machinery/doors/firedoor.dm index 39b9baae62..0e1a67316b 100644 --- a/code/game/machinery/doors/firedoor.dm +++ b/code/game/machinery/doors/firedoor.dm @@ -26,6 +26,7 @@ air_tight = TRUE attack_hand_is_action = TRUE attack_hand_speed = CLICK_CD_MELEE + can_open_with_hands = FALSE var/emergency_close_timer = 0 var/nextstate = null var/boltslocked = TRUE diff --git a/code/game/machinery/doors/poddoor.dm b/code/game/machinery/doors/poddoor.dm index 654dc13e1c..ba96cb5bf6 100644 --- a/code/game/machinery/doors/poddoor.dm +++ b/code/game/machinery/doors/poddoor.dm @@ -16,6 +16,7 @@ armor = list(MELEE = 50, BULLET = 100, LASER = 100, ENERGY = 100, BOMB = 50, BIO = 100, RAD = 100, FIRE = 100, ACID = 70) resistance_flags = FIRE_PROOF damage_deflection = 70 + can_open_with_hands = FALSE poddoor = TRUE /obj/machinery/door/poddoor/connect_to_shuttle(obj/docking_port/mobile/port, obj/docking_port/stationary/dock, idnum, override=FALSE) diff --git a/code/game/machinery/limbgrower.dm b/code/game/machinery/limbgrower.dm index 90408870a7..c3ce5732ab 100644 --- a/code/game/machinery/limbgrower.dm +++ b/code/game/machinery/limbgrower.dm @@ -330,7 +330,7 @@ var/obj/item/organ/genital/breasts/boobs = new(loc) if(features["has_breasts"]) boobs.color = sanitize_hexcolor(features["breasts_color"], 6, TRUE) - boobs.size = features["breasts_size"] + boobs.size = GLOB.breast_values[features["breasts_size"]] boobs.shape = features["breasts_shape"] if(!features["breasts_producing"]) boobs.genital_flags &= ~(GENITAL_FUID_PRODUCTION|CAN_CLIMAX_WITH|CAN_MASTURBATE_WITH) diff --git a/code/game/objects/items/crayons.dm b/code/game/objects/items/crayons.dm index f44860d769..08770eeadb 100644 --- a/code/game/objects/items/crayons.dm +++ b/code/game/objects/items/crayons.dm @@ -36,12 +36,11 @@ var/text_buffer = "" var/static/list/graffiti = list("amyjon","face","matt","revolution","engie","guy","end","dwarf","uboa","body","cyka","star","poseur tag","prolizard","antilizard", "tile") - var/static/list/symbols = list("danger","firedanger","electricdanger","biohazard","radiation","safe","evac","space","med","trade","shop","food","peace","like","skull","nay","heart","credit", "sonne", "ironguard", "scp", "falange") + var/static/list/symbols = list("danger","firedanger","electricdanger","biohazard","radiation","safe","evac","space","med","trade","shop","food","peace","like","skull","nay","heart","credit", "scp") var/static/list/drawings = list("smallbrush","brush","largebrush","splatter","snake","stickman","carp","ghost","clown","taser","disk","fireaxe","toolbox","corgi","cat","toilet","blueprint","beepsky","scroll","bottle","shotgun", "boobs", "booty", "bdragon") var/static/list/oriented = list("arrow","line","thinline","shortline","body","chevron","footprint","clawprint","pawprint") // These turn to face the same way as the drawer var/static/list/runes = list("rune1","rune2","rune3","rune4","rune5","rune6") - var/static/list/randoms = list(RANDOM_ANY, RANDOM_RUNE, RANDOM_ORIENTED, - RANDOM_NUMBER, RANDOM_GRAFFITI, RANDOM_LETTER, RANDOM_SYMBOL, RANDOM_PUNCTUATION, RANDOM_DRAWING) + var/static/list/randoms = list(RANDOM_ANY, RANDOM_RUNE, RANDOM_ORIENTED, RANDOM_NUMBER, RANDOM_GRAFFITI, RANDOM_LETTER, RANDOM_SYMBOL, RANDOM_PUNCTUATION, RANDOM_DRAWING) var/static/list/graffiti_large_h = list("yiffhell", "furrypride", "secborg", "paint") var/static/list/all_drawables = graffiti + symbols + drawings + oriented + runes + graffiti_large_h diff --git a/code/game/objects/structures/mirror.dm b/code/game/objects/structures/mirror.dm index ffb48ce633..d1af57bd28 100644 --- a/code/game/objects/structures/mirror.dm +++ b/code/game/objects/structures/mirror.dm @@ -184,7 +184,7 @@ var/custom_tone = input(user, "Choose your custom skin tone:", "Race change", default) as color|null if(custom_tone) var/temp_hsv = RGBtoHSV(new_s_tone) - if(ReadHSV(temp_hsv)[3] >= ReadHSV(MINIMUM_MUTANT_COLOR)[3]) + if(ReadHSV(temp_hsv)[3] >= ReadHSV(MINIMUM_MUTANT_COLOR)[3] || !CONFIG_GET(flag/character_color_limits)) //SPLURT edit to_chat(H,"Invalid color. Your color is not bright enough.") else H.skin_tone = custom_tone @@ -198,7 +198,7 @@ if(new_mutantcolor) var/temp_hsv = RGBtoHSV(new_mutantcolor) - if(ReadHSV(temp_hsv)[3] >= ReadHSV(MINIMUM_MUTANT_COLOR)[3]) // mutantcolors must be bright + if(ReadHSV(temp_hsv)[3] >= ReadHSV(MINIMUM_MUTANT_COLOR)[3] || !CONFIG_GET(flag/character_color_limits)) // mutantcolors must be bright //SPLURT edit H.dna.features["mcolor"] = sanitize_hexcolor(new_mutantcolor) else diff --git a/code/modules/arousal/genitals.dm b/code/modules/arousal/genitals.dm index 8b49c5225d..b13c515155 100644 --- a/code/modules/arousal/genitals.dm +++ b/code/modules/arousal/genitals.dm @@ -199,7 +199,7 @@ aroused_state = FALSE /obj/item/organ/genital/proc/generate_fluid(datum/reagents/R) - var/amount = clamp((fluid_rate * ((world.time - last_orgasmed) / (10 SECONDS)) * fluid_mult),0,fluid_max_volume) + var/amount = get_fluid() R.clear_reagents() R.maximum_volume = fluid_max_volume if(fluid_id) @@ -338,7 +338,7 @@ var/obj/item/organ/genital/G = A var/datum/sprite_accessory/S - var/size = G.size + var/size = G.size_to_state() switch(G.type) if(/obj/item/organ/genital/penis) S = GLOB.cock_shapes_list[G.shape] diff --git a/code/modules/arousal/organs/breasts.dm b/code/modules/arousal/organs/breasts.dm index 53892b24d8..4e65cb3200 100644 --- a/code/modules/arousal/organs/breasts.dm +++ b/code/modules/arousal/organs/breasts.dm @@ -8,7 +8,7 @@ icon = 'icons/obj/genitals/breasts.dmi' zone = BODY_ZONE_CHEST slot = ORGAN_SLOT_BREASTS - size = BREASTS_SIZE_DEF // "c". Refer to the breast_values static list below for the cups associated number values + size = 3 fluid_id = /datum/reagent/consumable/milk fluid_rate = MILK_RATE shape = DEF_BREASTS_SHAPE @@ -19,19 +19,12 @@ orgasm_verb = "leaking" fluid_transfer_factor = 0.5 layer_index = BREASTS_LAYER_INDEX - var/static/list/breast_values = list("a" = 1, "b" = 2, "c" = 3, "d" = 4, "e" = 5, "f" = 6, "g" = 7, "h" = 8, "i" = 9, "j" = 10, "k" = 11, "l" = 12, "m" = 13, "n" = 14, "o" = 15, "huge" = 16, "massive" = 17, "giga" = 25, "impossible" = 30, "flat" = 0) - var/cached_size //these two vars pertain size modifications and so should be expressed in NUMBERS. var/prev_size //former cached_size value, to allow update_size() to early return should be there no significant changes. -/obj/item/organ/genital/breasts/Initialize(mapload, do_update = TRUE) - if(do_update) - cached_size = breast_values[size] - prev_size = cached_size - return ..() - /obj/item/organ/genital/breasts/update_appearance() . = ..() var/lowershape = lowertext(shape) + var/size_state = size_to_state() switch(lowershape) if("pair") desc = "You see a pair of breasts." @@ -41,13 +34,13 @@ desc = "You see three sets of breasts, running from their chest to their belly." else desc = "You see some breasts, they seem to be quite exotic." - if(size == "huge") - desc = "You see [pick("some serious honkers", "a real set of badonkers", "some dobonhonkeros", "massive dohoonkabhankoloos", "two big old tonhongerekoogers", "a couple of giant bonkhonagahoogs", "a pair of humongous hungolomghnonoloughongous")]. Their volume is way beyond cupsize now, measuring in about [round(cached_size*(owner ? get_size(owner) : 1))]cm in diameter." + if(size_state == "huge") + desc = "You see [pick("some serious honkers", "a real set of badonkers", "some dobonhonkeros", "massive dohoonkabhankoloos", "two big old tonhongerekoogers", "a couple of giant bonkhonagahoogs", "a pair of humongous hungolomghnonoloughongous")]. Their volume is way beyond cupsize now, measuring in about [round(size*(owner ? get_size(owner) : 1))]cm in diameter." else - if (size == "flat") + if (size_state == "flat") desc += " They're very small and flatchested, however." else - desc += " You estimate that they're [uppertext(size)]-cups." + desc += " You estimate that they're [uppertext(size_state)]-cups." if((genital_flags & GENITAL_FUID_PRODUCTION) && aroused_state) var/datum/reagent/R = GLOB.chemical_reagents_list[fluid_id] @@ -55,7 +48,7 @@ desc += " They're leaking [lowertext(R.name)]." var/datum/sprite_accessory/S = GLOB.breasts_shapes_list[shape] var/icon_shape = S ? S.icon_state : "pair" - var/icon_size = clamp(breast_values[size], BREASTS_ICON_MIN_SIZE, BREASTS_ICON_MAX_SIZE) + var/icon_size = clamp(GLOB.breast_values[size_state], BREASTS_ICON_MIN_SIZE, BREASTS_ICON_MAX_SIZE) icon_state = "breasts_[icon_shape]_[icon_size]" if(owner) if(owner.dna.species.use_skintones && owner.dna.features["genitals_use_skintone"]) @@ -74,45 +67,49 @@ //this is far too lewd wah /obj/item/organ/genital/breasts/modify_size(modifier, min = -INFINITY, max = INFINITY) - var/new_value = clamp(cached_size + modifier, min, max) - if(new_value == cached_size) + var/new_value = clamp(size + modifier, max(min, min_size ? min_size : -INFINITY), min(max_size ? max_size : INFINITY, max)) + if(new_value == size) return - prev_size = cached_size - cached_size = new_value + prev_size = size + size = new_value update() ..() +/obj/item/organ/genital/breasts/size_to_state() + var/str_size + switch(size) + if(0) //flatchested + str_size = "flat" + if(1 to 8) //modest + str_size = GLOB.breast_values[size] + if(9 to 15) //massive + str_size = GLOB.breast_values[size] + if(16 to 17) //ridiculous + str_size = GLOB.breast_values[size] + if(18 to 24) //AWOOOOGAAAAAAA + str_size = "massive" + if(25 to 29) //AWOOOOOOGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA + str_size = "giga" + if(30 to INFINITY) //AWWWWWWWWWWWWWOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOGGGGGAAAAAAAAAAAAAAAAAAAAAA + str_size = "impossible" + return str_size + /obj/item/organ/genital/breasts/update_size()//wah - var/rounded_cached = round(cached_size) - if(cached_size < 0)//I don't actually know what round() does to negative numbers, so to be safe!!fixed + var/rounded_size = round(size) + var/size_state = size_to_state() + if(rounded_size < 0)//I don't actually know what round() does to negative numbers, so to be safe!!fixed if(owner) to_chat(owner, "You feel your breasts shrinking away from your body as your chest flattens out.") QDEL_IN(src, 1) return - switch(rounded_cached) - if(0) //flatchested - size = "flat" - if(1 to 8) //modest - size = breast_values[rounded_cached] - if(9 to 15) //massive - size = breast_values[rounded_cached] - if(16 to 17) //ridiculous - size = breast_values[rounded_cached] - if(18 to 24) //AWOOOOGAAAAAAA - size = "massive" - if(25 to 29) //AWOOOOOOGAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA - size = "giga" - if(30 to INFINITY) //AWWWWWWWWWWWWWOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOOGGGGGAAAAAAAAAAAAAAAAAAAAAA - size = "impossible" - - if((rounded_cached < 18 || rounded_cached == 25 || rounded_cached == 30) && owner )//Because byond doesn't count from 0, I have to do this. + if((rounded_size < 18 || rounded_size == 25 || rounded_size == 30) && owner )//Because byond doesn't count from 0, I have to do this. var/mob/living/carbon/human/H = owner var/r_prev_size = round(prev_size) - if (rounded_cached > r_prev_size) - to_chat(H, "Your breasts [pick("swell up to", "flourish into", "expand into", "burst forth into", "grow eagerly into", "amplify into")] a [uppertext(size)]-cup.") - else if (rounded_cached < r_prev_size) - to_chat(H, "Your breasts [pick("shrink down to", "decrease into", "diminish into", "deflate into", "shrivel regretfully into", "contracts into")] a [uppertext(size)]-cup.") + if (rounded_size > r_prev_size) + to_chat(H, "Your breasts [pick("swell up to", "flourish into", "expand into", "burst forth into", "grow eagerly into", "amplify into")] a [uppertext(size_state)]-cup.") + else if (rounded_size < r_prev_size) + to_chat(H, "Your breasts [pick("shrink down to", "decrease into", "diminish into", "deflate into", "shrivel regretfully into", "contracts into")] a [uppertext(size_state)]-cup.") /obj/item/organ/genital/breasts/get_features(mob/living/carbon/human/H) var/datum/dna/D = H.dna @@ -120,16 +117,13 @@ color = SKINTONE2HEX(H.skin_tone) else color = "#[D.features["breasts_color"]]" - size = D.features["breasts_size"] + size = GLOB.breast_values[D.features["breasts_size"]] + max_size = D.features["breasts_max_size"] + min_size = D.features["breasts_min_size"] shape = D.features["breasts_shape"] if(!D.features["breasts_producing"]) genital_flags &= ~ (GENITAL_FUID_PRODUCTION|CAN_CLIMAX_WITH|CAN_MASTURBATE_WITH) - if(!isnum(size)) - cached_size = breast_values[size] - else - cached_size = size - size = breast_values[size] - prev_size = cached_size + prev_size = size toggle_visibility(D.features["breasts_visibility"], FALSE) if(D.features["breasts_stuffing"]) toggle_visibility(GEN_ALLOW_EGG_STUFFING, FALSE) diff --git a/code/modules/arousal/organs/butt.dm b/code/modules/arousal/organs/butt.dm index 526bc28ff1..bcad9af8fb 100644 --- a/code/modules/arousal/organs/butt.dm +++ b/code/modules/arousal/organs/butt.dm @@ -7,6 +7,7 @@ slot = ORGAN_SLOT_BUTT w_class = 3 size = 0 + max_size = BUTT_SIZE_MAX var/size_name = "nonexistent" shape = "Pair" //turn this into a default constant if for some inexplicable reason we get more than one butt type but I doubt it. genital_flags = UPDATE_OWNER_APPEARANCE|GENITAL_UNDIES_HIDDEN|CAN_CUM_INTO|HAS_EQUIPMENT @@ -15,8 +16,8 @@ var/prev_size //former size value, to allow update_size() to early return should be there no significant changes. layer_index = BUTT_LAYER_INDEX -/obj/item/organ/genital/butt/modify_size(modifier, min = -INFINITY, max = BUTT_SIZE_MAX) - var/new_value = clamp(size_cached + modifier, min, max) +/obj/item/organ/genital/butt/modify_size(modifier, min = -INFINITY, max = INFINITY) + var/new_value = clamp(size_cached + modifier, max(min, min_size ? min_size : -INFINITY), min(max_size ? max_size : INFINITY, max)) if(new_value == size_cached) return prev_size = size_cached @@ -88,6 +89,8 @@ else color = "#[D.features["butt_color"]]" size = D.features["butt_size"] + max_size = D.features["butt_max_size"] + min_size = D.features["butt_min_size"] prev_size = size size_cached = size toggle_visibility(D.features["butt_visibility"], FALSE) diff --git a/code/modules/arousal/organs/penis.dm b/code/modules/arousal/organs/penis.dm index 9e986d9bbb..46dc8bb19c 100644 --- a/code/modules/arousal/organs/penis.dm +++ b/code/modules/arousal/organs/penis.dm @@ -14,18 +14,20 @@ shape = DEF_COCK_SHAPE size = 2 //arbitrary value derived from length and diameter for sprites. layer_index = PENIS_LAYER_INDEX - var/length = 6 //inches + var/length = 6 //inches + var/max_length = 9 + var/min_length = 2 var/prev_length = 6 //really should be renamed to prev_length var/diameter = 4.38 var/diameter_ratio = COCK_DIAMETER_RATIO_DEF //0.25; check citadel_defines.dm /obj/item/organ/genital/penis/modify_size(modifier, min = -INFINITY, max = INFINITY) - var/new_value = clamp(length + modifier, min, max) + var/new_value = clamp(length + modifier, max(min, min_size ? min_size : -INFINITY), min(max_length ? max_length : INFINITY, max)) if(new_value == length) return prev_length = length - length = clamp(length + modifier, min, max) + length = new_value update() ..() @@ -95,6 +97,8 @@ else color = "#[D.features["cock_color"]]" length = D.features["cock_length"] + max_length = D.features["cock_max_length"] + min_length = D.features["cock_min_length"] diameter_ratio = D.features["cock_diameter_ratio"] shape = D.features["cock_shape"] prev_length = length diff --git a/code/modules/client/preferences.dm b/code/modules/client/preferences.dm index c3e981c9df..dec8b9b37b 100644 --- a/code/modules/client/preferences.dm +++ b/code/modules/client/preferences.dm @@ -844,6 +844,8 @@ GLOBAL_LIST_EMPTY(preferences_datums) tauric_shape = TRUE dat += "Penis Shape: [features["cock_shape"]][tauric_shape ? " (Taur)" : ""]" dat += "Penis Length: [features["cock_length"]] inch(es)" + dat += "Max Length:[features["cock_max_length"] ? features["cock_max_length"] : "Disabled"]" + dat += "Min Length:[features["cock_min_length"] ? features["cock_min_length"] : "Disabled"]" dat += "Diameter Ratio: [features["cock_diameter_ratio"]]" //SPLURT Edit dat += "Penis Visibility:[features["cock_visibility"]]" dat += "Egg Stuffing:[features["cock_stuffing"] == TRUE ? "Yes" : "No"]" //SPLURT Edit @@ -859,6 +861,8 @@ GLOBAL_LIST_EMPTY(preferences_datums) dat += "Testicles Visibility:[features["balls_visibility"]]" //SPLURT Edit dat += "Egg Stuffing:[features["balls_stuffing"] == TRUE ? "Yes" : "No"]" + dat += "Max Size:[features["balls_max_size"] ? features["balls_max_size"] : "Disabled"]" + dat += "Min Size:[features["balls_min_size"] ? features["balls_min_size"] : "Disabled"]" dat += "Produces:" var/datum/reagent/balls_fluid = find_reagent_object_from_type(features["balls_fluid"]) if(balls_fluid && (balls_fluid in GLOB.genital_fluids_list)) @@ -907,6 +911,8 @@ GLOBAL_LIST_EMPTY(preferences_datums) dat += "Lactates:[features["breasts_producing"] == TRUE ? "Yes" : "No"]" //SPLURT Edit dat += "Egg Stuffing:[features["breasts_stuffing"] == TRUE ? "Yes" : "No"]" + dat += "Max Size:[features["breasts_max_size"] ? features["breasts_max_size"] : "Disabled"]" + dat += "Min Size:[features["breasts_min_size"] ? features["breasts_min_size"] : "Disabled"]" if(features["breasts_producing"] == TRUE) dat += "Produces:" var/datum/reagent/breasts_fluid = find_reagent_object_from_type(features["breasts_fluid"]) @@ -930,6 +936,8 @@ GLOBAL_LIST_EMPTY(preferences_datums) dat += "Butt Visibility:[features["butt_visibility"]]" //SPLURT Edit dat += "Egg Stuffing:[features["butt_stuffing"] == TRUE ? "Yes" : "No"]" + dat += "Max Size:[features["butt_max_size"] ? features["butt_max_size"] : "Disabled"]" + dat += "Min Size:[features["butt_min_size"] ? features["butt_min_size"] : "Disabled"]" dat += "Butthole Sprite:[features["has_anus"] == TRUE ? "Yes" : "No"]" if(features["has_anus"]) dat += "Butthole Color:
" @@ -953,6 +961,8 @@ GLOBAL_LIST_EMPTY(preferences_datums) dat += "Color:
" dat += "#[features["belly_color"]] Change
" dat += "Belly Size:[features["belly_size"]]" + dat += "Max Size:[features["belly_max_size"] ? features["belly_max_size"] : "Disabled" ]" + dat += "Min Size:[features["belly_min_size"] ? features["belly_min_size"] : "Disabled" ]" dat += "Belly Visibility:[features["belly_visibility"]]" dat += "Egg Stuffing:[features["belly_stuffing"] == TRUE ? "Yes" : "No"]" dat += "" @@ -2383,9 +2393,9 @@ GLOBAL_LIST_EMPTY(preferences_datums) var/new_mutantcolor = input(user, "Choose your character's alien/mutant color:", "Character Preference","#"+features["mcolor"]) as color|null if(new_mutantcolor) var/temp_hsv = RGBtoHSV(new_mutantcolor) - if(new_mutantcolor == "#000000") + if(new_mutantcolor == "#000000" && features["mcolor"] != pref_species.default_color) //SPLURT EDIT features["mcolor"] = pref_species.default_color - else if((MUTCOLORS_PARTSONLY in pref_species.species_traits) || ReadHSV(temp_hsv)[3] >= ReadHSV(MINIMUM_MUTANT_COLOR)[3]) // mutantcolors must be bright, but only if they affect the skin + else if((MUTCOLORS_PARTSONLY in pref_species.species_traits) || ReadHSV(temp_hsv)[3] >= ReadHSV(MINIMUM_MUTANT_COLOR)[3] || !CONFIG_GET(flag/character_color_limits)) // mutantcolors must be bright, but only if they affect the skin //SPLURT EDIT features["mcolor"] = sanitize_hexcolor(new_mutantcolor, 6) else to_chat(user, "Invalid color. Your color is not bright enough.") @@ -2394,9 +2404,9 @@ GLOBAL_LIST_EMPTY(preferences_datums) var/new_mutantcolor = input(user, "Choose your character's secondary alien/mutant color:", "Character Preference","#"+features["mcolor2"]) as color|null if(new_mutantcolor) var/temp_hsv = RGBtoHSV(new_mutantcolor) - if(new_mutantcolor == "#000000") + if(new_mutantcolor == "#000000" && features["mcolor2"] != pref_species.default_color) //SPLURT EDIT features["mcolor2"] = pref_species.default_color - else if((MUTCOLORS_PARTSONLY in pref_species.species_traits) || ReadHSV(temp_hsv)[3] >= ReadHSV(MINIMUM_MUTANT_COLOR)[3]) // mutantcolors must be bright, but only if they affect the skin + else if((MUTCOLORS_PARTSONLY in pref_species.species_traits) || ReadHSV(temp_hsv)[3] >= ReadHSV(MINIMUM_MUTANT_COLOR)[3] || !CONFIG_GET(flag/character_color_limits)) // mutantcolors must be bright, but only if they affect the skin //SPLURT EDIT features["mcolor2"] = sanitize_hexcolor(new_mutantcolor, 6) else to_chat(user, "Invalid color. Your color is not bright enough.") @@ -2405,9 +2415,9 @@ GLOBAL_LIST_EMPTY(preferences_datums) var/new_mutantcolor = input(user, "Choose your character's tertiary alien/mutant color:", "Character Preference","#"+features["mcolor3"]) as color|null if(new_mutantcolor) var/temp_hsv = RGBtoHSV(new_mutantcolor) - if(new_mutantcolor == "#000000") + if(new_mutantcolor == "#000000" && features["mcolor3"] != pref_species.default_color) //SPLURT EDIT features["mcolor3"] = pref_species.default_color - else if((MUTCOLORS_PARTSONLY in pref_species.species_traits) || ReadHSV(temp_hsv)[3] >= ReadHSV(MINIMUM_MUTANT_COLOR)[3]) // mutantcolors must be bright, but only if they affect the skin + else if((MUTCOLORS_PARTSONLY in pref_species.species_traits) || ReadHSV(temp_hsv)[3] >= ReadHSV(MINIMUM_MUTANT_COLOR)[3] || !CONFIG_GET(flag/character_color_limits)) // mutantcolors must be bright, but only if they affect the skin //SPLURT EDIT features["mcolor3"] = sanitize_hexcolor(new_mutantcolor, 6) else to_chat(user, "Invalid color. Your color is not bright enough.") @@ -2421,9 +2431,9 @@ GLOBAL_LIST_EMPTY(preferences_datums) var/new_neckfire_color = input(user, "Choose your fire's color:", "Character Preference", "#"+features["neckfire_color"]) as color|null if(new_neckfire_color) var/temp_hsv = RGBtoHSV(new_neckfire_color) - if(new_neckfire_color == "#000000") + if(new_neckfire_color == "#000000" && features["neckfire_color"] != pref_species.default_color) //SPLURT EDIT features["neckfire_color"] = pref_species.default_color - else if(ReadHSV(temp_hsv)[3] >= ReadHSV(MINIMUM_MUTANT_COLOR)[3]) + else if(ReadHSV(temp_hsv)[3] >= ReadHSV(MINIMUM_MUTANT_COLOR)[3] || !CONFIG_GET(flag/character_color_limits)) //SPLURT EDIT features["neckfire_color"] = sanitize_hexcolor(new_neckfire_color, 6) else to_chat(user,"Invalid color. Your color is not bright enough.") @@ -2564,7 +2574,7 @@ GLOBAL_LIST_EMPTY(preferences_datums) if("horns_color") var/new_horn_color = input(user, "Choose your character's horn colour:", "Character Preference","#"+features["horns_color"]) as color|null if(new_horn_color) - if (new_horn_color == "#000000") + if (new_horn_color == "#000000" && features["horns_color"] != "85615A") //SPLURT EDIT features["horns_color"] = "85615A" else features["horns_color"] = sanitize_hexcolor(new_horn_color, 6) @@ -2578,7 +2588,7 @@ GLOBAL_LIST_EMPTY(preferences_datums) if("wings_color") var/new_wing_color = input(user, "Choose your character's wing colour:", "Character Preference","#"+features["wings_color"]) as color|null if(new_wing_color) - if (new_wing_color == "#000000") + if (new_wing_color == "#000000" && features["wings_color"] != "#FFFFFF") //SPLURT EDIT features["wings_color"] = "#FFFFFF" else features["wings_color"] = sanitize_hexcolor(new_wing_color, 6) @@ -2654,7 +2664,7 @@ GLOBAL_LIST_EMPTY(preferences_datums) var/custom_tone = input(user, "Choose your custom skin tone:", "Character Preference", default) as color|null if(custom_tone) var/temp_hsv = RGBtoHSV(custom_tone) - if(ReadHSV(temp_hsv)[3] < ReadHSV("#333333")[3]) // rgb(50,50,50) + if(ReadHSV(temp_hsv)[3] < ReadHSV("#333333")[3] && CONFIG_GET(flag/character_color_limits)) // rgb(50,50,50) //SPLURT EDIT to_chat(user,"Invalid color. Your color is not bright enough.") else use_custom_skin_tone = TRUE @@ -2749,9 +2759,9 @@ GLOBAL_LIST_EMPTY(preferences_datums) var/new_feature_color = input(user, "Choose your character's mutant part colour:", "Character Preference","#"+features[href_list["preference"]]) as color|null if(new_feature_color) var/temp_hsv = RGBtoHSV(new_feature_color) - if(new_feature_color == "#000000") + if(new_feature_color == "#000000" && features[href_list["preference"]] != pref_species.default_color) //SPLURT EDIT features[href_list["preference"]] = pref_species.default_color - else if(ReadHSV(temp_hsv)[3] >= ReadHSV(MINIMUM_MUTANT_COLOR)[3]) + else if(ReadHSV(temp_hsv)[3] >= ReadHSV(MINIMUM_MUTANT_COLOR)[3] || !CONFIG_GET(flag/character_color_limits)) //SPLURT EDIT features[href_list["preference"]] = sanitize_hexcolor(new_feature_color, 6) else to_chat(user,"Invalid color. Your color is not bright enough.") @@ -2778,9 +2788,9 @@ GLOBAL_LIST_EMPTY(preferences_datums) var/new_cockcolor = input(user, "Penis color:", "Character Preference","#"+features["cock_color"]) as color|null if(new_cockcolor) var/temp_hsv = RGBtoHSV(new_cockcolor) - if(new_cockcolor == "#000000") + if(new_cockcolor == "#000000" && features["cock_color"] != pref_species.default_color) //SPLURT EDIT features["cock_color"] = pref_species.default_color - else if(ReadHSV(temp_hsv)[3] >= ReadHSV(MINIMUM_MUTANT_COLOR)[3]) + else if(ReadHSV(temp_hsv)[3] >= ReadHSV(MINIMUM_MUTANT_COLOR)[3] || !CONFIG_GET(flag/character_color_limits)) //SPLURT EDIT features["cock_color"] = sanitize_hexcolor(new_cockcolor, 6) else to_chat(user,"Invalid color. Your color is not bright enough.") @@ -2825,9 +2835,9 @@ GLOBAL_LIST_EMPTY(preferences_datums) var/new_ballscolor = input(user, "Testicles Color:", "Character Preference","#"+features["balls_color"]) as color|null if(new_ballscolor) var/temp_hsv = RGBtoHSV(new_ballscolor) - if(new_ballscolor == "#000000") + if(new_ballscolor == "#000000" && features["balls_color"] != pref_species.default_color) //SPLURT EDIT features["balls_color"] = pref_species.default_color - else if(ReadHSV(temp_hsv)[3] >= ReadHSV(MINIMUM_MUTANT_COLOR)[3]) + else if(ReadHSV(temp_hsv)[3] >= ReadHSV(MINIMUM_MUTANT_COLOR)[3] || !CONFIG_GET(flag/character_color_limits)) //SPLURT EDIT features["balls_color"] = sanitize_hexcolor(new_ballscolor, 6) else to_chat(user,"Invalid color. Your color is not bright enough.") @@ -2868,9 +2878,9 @@ GLOBAL_LIST_EMPTY(preferences_datums) var/new_breasts_color = input(user, "Breast Color:", "Character Preference","#"+features["breasts_color"]) as color|null if(new_breasts_color) var/temp_hsv = RGBtoHSV(new_breasts_color) - if(new_breasts_color == "#000000") + if(new_breasts_color == "#000000" && features["breasts_color"] != pref_species.default_color) //SPLURT EDIT features["breasts_color"] = pref_species.default_color - else if(ReadHSV(temp_hsv)[3] >= ReadHSV(MINIMUM_MUTANT_COLOR)[3]) + else if(ReadHSV(temp_hsv)[3] >= ReadHSV(MINIMUM_MUTANT_COLOR)[3] || !CONFIG_GET(flag/character_color_limits)) //SPLURT EDIT features["breasts_color"] = sanitize_hexcolor(new_breasts_color, 6) else to_chat(user,"Invalid color. Your color is not bright enough.") @@ -2900,9 +2910,9 @@ GLOBAL_LIST_EMPTY(preferences_datums) var/new_vagcolor = input(user, "Vagina color:", "Character Preference","#"+features["vag_color"]) as color|null if(new_vagcolor) var/temp_hsv = RGBtoHSV(new_vagcolor) - if(new_vagcolor == "#000000") + if(new_vagcolor == "#000000" && features["vag_color"] != pref_species.default_color) //SPLURT EDIT features["vag_color"] = pref_species.default_color - else if(ReadHSV(temp_hsv)[3] >= ReadHSV(MINIMUM_MUTANT_COLOR)[3]) + else if(ReadHSV(temp_hsv)[3] >= ReadHSV(MINIMUM_MUTANT_COLOR)[3] || !CONFIG_GET(flag/character_color_limits)) //SPLURT EDIT features["vag_color"] = sanitize_hexcolor(new_vagcolor, 6) else to_chat(user,"Invalid color. Your color is not bright enough.") @@ -2926,9 +2936,9 @@ GLOBAL_LIST_EMPTY(preferences_datums) var/new_bellycolor = input(user, "Belly Color:", "Character Preference", "#"+features["belly_color"]) as color|null if(new_bellycolor) var/temp_hsv = RGBtoHSV(new_bellycolor) - if(new_bellycolor == "#000000") + if(new_bellycolor == "#000000" && features["belly_color"] != pref_species.default_color) //SPLURT EDIT features["belly_color"] = pref_species.default_color - else if(ReadHSV(temp_hsv)[3] >= ReadHSV(MINIMUM_MUTANT_COLOR)[3]) + else if(ReadHSV(temp_hsv)[3] >= ReadHSV(MINIMUM_MUTANT_COLOR)[3] || !CONFIG_GET(flag/character_color_limits)) //SPLURT EDIT features["belly_color"] = sanitize_hexcolor(new_bellycolor, 6) else to_chat(user,"Invalid color. Your color is not bright enough.") @@ -2937,9 +2947,9 @@ GLOBAL_LIST_EMPTY(preferences_datums) var/new_buttcolor = input(user, "Butt color:", "Character Preference","#"+features["butt_color"]) as color|null if(new_buttcolor) var/temp_hsv = RGBtoHSV(new_buttcolor) - if(new_buttcolor == "#000000") + if(new_buttcolor == "#000000" && features["butt_color"] != pref_species.default_color) //SPLURT EDIT features["butt_color"] = pref_species.default_color - else if(ReadHSV(temp_hsv)[3] >= ReadHSV(MINIMUM_MUTANT_COLOR)[3]) + else if(ReadHSV(temp_hsv)[3] >= ReadHSV(MINIMUM_MUTANT_COLOR)[3] || !CONFIG_GET(flag/character_color_limits)) //SPLURT EDIT features["butt_color"] = sanitize_hexcolor(new_buttcolor, 6) else to_chat(user,"Invalid color. Your color is not bright enough.") @@ -2948,9 +2958,9 @@ GLOBAL_LIST_EMPTY(preferences_datums) var/new_anuscolor = input(user, "Butthole color:", "Character Preference", "#"+features["anus_color"]) as color|null if(new_anuscolor) var/temp_hsv = RGBtoHSV(new_anuscolor) - if(new_anuscolor == "#000000") + if(new_anuscolor == "#000000" && features["anus_color"] != pref_species.default_color) //SPLURT EDIT features["anus_color"] = pref_species.default_color - else if(ReadHSV(temp_hsv)[3] >= ReadHSV(MINIMUM_MUTANT_COLOR)[3]) + else if(ReadHSV(temp_hsv)[3] >= ReadHSV(MINIMUM_MUTANT_COLOR)[3] || !CONFIG_GET(flag/character_color_limits)) //SPLURT EDIT features["anus_color"] = sanitize_hexcolor(new_anuscolor, 6) else to_chat(user, "Invalid color. Your color is not bright enough.") @@ -2990,6 +3000,82 @@ GLOBAL_LIST_EMPTY(preferences_datums) if(n_vis) features["belly_visibility"] = n_vis + if("cock_max_length") + var/max_B = CONFIG_GET(number/penis_max_inches_prefs) + var/new_size = input(user, "Max size:\n([features["cock_length"]]-[max_B])(0 = disabled)", "Character Preference") as num|null + if(new_size) + features["cock_max_length"] = clamp(round(new_size), features["cock_length"], max_B) + else + features -= "cock_max_length" + + if("balls_max_size") + var/new_size = input(user, "Max size:\n([BALLS_SIZE_MIN]-[BALLS_SIZE_MAX])(0 = disabled)", "Character Preference") as num|null + if(new_size) + features["balls_max_size"] = clamp(round(new_size), BALLS_SIZE_MIN, BALLS_SIZE_MAX) + else + features -= "balls_max_size" + + if("breasts_max_size") + var/new_size = input(user, "Breast Max Size (cancel to disable)", "Character Preference") as null|anything in GLOB.breast_values + if(new_size) + features["breasts_max_size"] = new_size + else + features -= "breasts_max_size" + + if("belly_max_size") + var/max_B = CONFIG_GET(number/belly_max_size_prefs) + var/new_size = input(user, "Max size:\n([features["belly_size"]]-[max_B])(0 = disabled)", "Character Preference") as num|null + if(new_size) + features["belly_max_size"] = clamp(round(new_size), features["belly_size"], max_B) + else + features -= "belly_max_size" + + if("butt_max_size") + var/max_B = CONFIG_GET(number/butt_max_size_prefs) + var/new_size = input(user, "Max size:\n([features["butt_size"]]-[max_B])(0 = disabled)", "Character Preference") as num|null + if(new_size) + features["butt_max_size"] = clamp(round(new_size), features["butt_size"], max_B) + else + features -= "butt_max_size" + + if("cock_min_length") + var/min_B = CONFIG_GET(number/penis_min_inches_prefs) + var/new_size = input(user, "Min size:\n([min_B]-[features["cock_length"]])(0 = disabled)", "Character Preference") as num|null + if(new_size) + features["cock_min_length"] = clamp(round(new_size), min_B, features["cock_length"]) + else + features -= "cock_min_length" + + if("balls_min_size") + var/new_size = input(user, "Min size:\n([BALLS_SIZE_MIN]-[BALLS_SIZE_MAX])(0 = disabled)", "Character Preference") as num|null + if(new_size) + features["balls_min_size"] = clamp(round(new_size), BALLS_SIZE_MIN, BALLS_SIZE_MAX) + else + features -= "balls_min_size" + + if("breasts_min_size") + var/new_size = input(user, "Breast Min Size (cancel to disable)", "Character Preference") as null|anything in GLOB.breast_values + if(new_size) + features["breasts_min_size"] = new_size + else + features -= "breasts_min_size" + + if("belly_min_size") + var/min_B = CONFIG_GET(number/belly_min_size_prefs) + var/new_size = input(user, "Min size:\n([min_B]-[features["belly_size"]])(0 = disabled)", "Character Preference") as num|null + if(new_size) + features["belly_min_size"] = clamp(round(new_size), min_B, features["belly_size"]) + else + features -= "belly_min_size" + + if("butt_min_size") + var/min_B = CONFIG_GET(number/butt_min_size_prefs) + var/new_size = input(user, "Min size:\n([min_B]-[features["butt_size"]])(0 = disabled)", "Character Preference") as num|null + if(new_size) + features["butt_min_size"] = clamp(round(new_size), min_B, features["butt_size"]) + else + features -= "butt_min_size" + if("ooccolor") var/new_ooccolor = input(user, "Choose your OOC colour:", "Game Preference",ooccolor) as color|null if(new_ooccolor) @@ -3252,7 +3338,7 @@ GLOBAL_LIST_EMPTY(preferences_datums) var/new_marking_color = input(user, "Choose your character's marking color:", "Character Preference","#"+color_list[color_number]) as color|null if(new_marking_color) var/temp_hsv = RGBtoHSV(new_marking_color) - if((MUTCOLORS_PARTSONLY in pref_species.species_traits) || ReadHSV(temp_hsv)[3] >= ReadHSV(MINIMUM_MUTANT_COLOR)[3]) // mutantcolors must be bright, but only if they affect the skin + if((MUTCOLORS_PARTSONLY in pref_species.species_traits) || ReadHSV(temp_hsv)[3] >= ReadHSV(MINIMUM_MUTANT_COLOR)[3] || !CONFIG_GET(flag/character_color_limits)) // mutantcolors must be bright, but only if they affect the skin //SPLURT EDIT color_list[color_number] = "#[sanitize_hexcolor(new_marking_color, 6)]" else to_chat(user, "Invalid color. Your color is not bright enough.") @@ -4173,3 +4259,4 @@ GLOBAL_LIST_EMPTY(preferences_datums) #undef DEFAULT_SLOT_AMT #undef HANDS_SLOT_AMT #undef BACKPACK_SLOT_AMT + diff --git a/code/modules/client/preferences_savefile.dm b/code/modules/client/preferences_savefile.dm index 9811f7a2be..aad17eb424 100644 --- a/code/modules/client/preferences_savefile.dm +++ b/code/modules/client/preferences_savefile.dm @@ -1054,6 +1054,28 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car S["feature_inert_eggs"] >> features["inert_eggs"] + if(S["features_cock_max_length"]) + S["features_cock_max_length"] >> features["cock_max_length"] + if(S["features_balls_max_size"]) + S["features_balls_max_size"] >> features["balls_max_size"] + if(S["features_breasts_max_size"]) + S["features_breasts_max_size"] >> features["breasts_max_size"] + if(S["features_belly_max_size"]) + S["features_belly_max_size"] >> features["belly_max_size"] + if(S["features_butt_max_size"]) + S["features_butt_max_size"] >> features["butt_max_size"] + + if(S["features_cock_min_length"]) + S["features_cock_min_length"] >> features["cock_min_length"] + if(S["features_balls_min_size"]) + S["features_balls_min_size"] >> features["balls_min_size"] + if(S["features_breasts_min_size"]) + S["features_breasts_min_size"] >> features["breasts_min_size"] + if(S["features_belly_min_size"]) + S["features_belly_min_size"] >> features["belly_min_size"] + if(S["features_butt_min_size"]) + S["features_butt_min_size"] >> features["butt_min_size"] + var/char_vr_path = "[vr_path]/character_[default_slot]_v2.json" if(fexists(char_vr_path)) var/list/json_from_file = json_decode(file2text(char_vr_path)) @@ -1413,6 +1435,19 @@ SAVEFILE UPDATING/VERSIONING - 'Simplified', or rather, more coder-friendly ~Car WRITE_FILE(S["feature_inert_eggs"], features["inert_eggs"]) + + WRITE_FILE(S["features_cock_max_length"], features["cock_max_length"]) + WRITE_FILE(S["features_balls_max_size"], features["balls_max_size"]) + WRITE_FILE(S["features_breasts_max_size"], features["breasts_max_size"]) + WRITE_FILE(S["features_belly_max_size"], features["belly_max_size"]) + WRITE_FILE(S["features_butt_max_size"], features["butt_max_size"]) + + WRITE_FILE(S["features_cock_min_length"], features["cock_min_length"]) + WRITE_FILE(S["features_balls_min_size"], features["balls_min_size"]) + WRITE_FILE(S["features_breasts_min_size"], features["breasts_min_size"]) + WRITE_FILE(S["features_belly_min_size"], features["belly_min_size"]) + WRITE_FILE(S["features_butt_min_size"], features["butt_min_size"]) + WRITE_FILE(S["feature_neckfire"], features["neckfire"]) WRITE_FILE(S["feature_neckfire_color"], features["neckfire_color"]) diff --git a/code/modules/mob/living/carbon/human/innate_abilities/customization.dm b/code/modules/mob/living/carbon/human/innate_abilities/customization.dm index 1aa5e56451..1807759812 100644 --- a/code/modules/mob/living/carbon/human/innate_abilities/customization.dm +++ b/code/modules/mob/living/carbon/human/innate_abilities/customization.dm @@ -29,7 +29,7 @@ var/new_color = input(owner, "Choose your skin color:", "Race change","#"+H.dna.features["mcolor"]) as color|null if(new_color) var/temp_hsv = RGBtoHSV(new_color) - if(ReadHSV(temp_hsv)[3] >= ReadHSV(MINIMUM_MUTANT_COLOR)[3]) // mutantcolors must be bright + if(ReadHSV(temp_hsv)[3] >= ReadHSV(MINIMUM_MUTANT_COLOR)[3] || !CONFIG_GET(flag/character_color_limits)) // mutantcolors must be bright //SPLURT EDIT H.dna.features["mcolor"] = sanitize_hexcolor(new_color, 6) H.update_body() H.update_hair() @@ -117,7 +117,7 @@ if(new_snout) H.dna.features["mam_snouts"] = new_snout H.update_body() - + else if (select_alteration == "Wings") var/new_color = input(owner, "Choose your wing color:", "Race change","#"+H.dna.features["wings_color"]) as color|null if(new_color) diff --git a/code/modules/mob/mob.dm b/code/modules/mob/mob.dm index 4703712f31..cb815bb2c3 100644 --- a/code/modules/mob/mob.dm +++ b/code/modules/mob/mob.dm @@ -404,7 +404,7 @@ * * overridden here and in /mob/dead/observer for different point span classes and sanity checks */ -/mob/verb/pointed(atom/A as mob|obj|turf in fov_view()) +/mob/verb/pointed(atom/A as mob|obj|turf in fov_view(), params = "" as text) set name = "Point To" set category = "Object" @@ -419,7 +419,25 @@ var/turf/our_tile = get_turf(src) var/obj/visual = new /obj/effect/temp_visual/point(our_tile, invisibility) - animate(visual, pixel_x = (tile.x - our_tile.x) * world.icon_size + A.pixel_x, pixel_y = (tile.y - our_tile.y) * world.icon_size + A.pixel_y, time = 1.7, easing = EASE_OUT) + + /// Set position + var/final_x = (tile.x - our_tile.x) * world.icon_size + A.pixel_x + var/final_y = (tile.y - our_tile.y) * world.icon_size + A.pixel_y + var/list/click_params = params2list(params) + if(length(click_params) && click_params["screen-loc"]) + var/list/actual_view = getviewsize(client ? client.view : world.view) + var/list/split_coords = splittext(click_params["screen-loc"], ",") + final_x = (text2num(splittext(split_coords[1], ":")[1]) - actual_view[1] / 2) * world.icon_size + (text2num(splittext(split_coords[1], ":")[2]) - world.icon_size) + final_y = (text2num(splittext(split_coords[2], ":")[1]) - actual_view[2] / 2) * world.icon_size + (text2num(splittext(split_coords[2], ":")[2]) - world.icon_size) + // + + /// Set rotation + var/matrix/rotated_matrix = new() + rotated_matrix.TurnTo(0, Get_Pixel_Angle(-final_y, -final_x)) + visual.transform = rotated_matrix + // + + animate(visual, pixel_x = final_x, pixel_y = final_y, time = 1.7, easing = EASE_OUT) SEND_SIGNAL(src, COMSIG_MOB_POINTED, A) return TRUE diff --git a/code/modules/tgs/includes.dm b/code/modules/tgs/includes.dm index 25e1b8421a..23b714f9d0 100644 --- a/code/modules/tgs/includes.dm +++ b/code/modules/tgs/includes.dm @@ -13,6 +13,9 @@ #include "v5\_defines.dm" #include "v5\api.dm" +#include "v5\bridge.dm" +#include "v5\chunking.dm" #include "v5\commands.dm" #include "v5\serializers.dm" +#include "v5\topic.dm" #include "v5\undefs.dm" diff --git a/code/modules/tgs/v5/README.md b/code/modules/tgs/v5/README.md index 619b58cd72..a8a0c748e7 100644 --- a/code/modules/tgs/v5/README.md +++ b/code/modules/tgs/v5/README.md @@ -5,6 +5,9 @@ This DMAPI implements bridge requests using HTTP GET requests to TGS. It has no - [__interop_version.dm](./__interop_version.dm) contains the version of the API used between the DMAPI and TGS. - [_defines.dm](./_defines.dm) contains constant definitions. - [api.dm](./api.dm) contains the bulk of the API code. +- [bridge.dm](./bridge.dm) contains functions related to making bridge requests. +- [chunking.dm](./chunking.dm) contains common function for splitting large raw data sets into chunks BYOND can natively process. - [commands.dm](./commands.dm) contains functions relating to `/datum/tgs_chat_command`s. - [serializers.dm](./serializers.dm) contains function to help convert interop `/datum`s into a JSON encodable `list()` format. +- [topic.dm](./topic.dm) contains functions related to processing topic requests. - [undefs.dm](./undefs.dm) Undoes the work of `_defines.dm`. diff --git a/code/modules/tgs/v5/__interop_version.dm b/code/modules/tgs/v5/__interop_version.dm index d0ac7e92ea..6ef7c86ef7 100644 --- a/code/modules/tgs/v5/__interop_version.dm +++ b/code/modules/tgs/v5/__interop_version.dm @@ -1 +1 @@ -"5.5.0" +"5.6.0" diff --git a/code/modules/tgs/v5/_defines.dm b/code/modules/tgs/v5/_defines.dm index 7f31c23ef4..a3f949081f 100644 --- a/code/modules/tgs/v5/_defines.dm +++ b/code/modules/tgs/v5/_defines.dm @@ -4,16 +4,29 @@ #define DMAPI5_BRIDGE_DATA "data" #define DMAPI5_TOPIC_DATA "tgs_data" +#define DMAPI5_BRIDGE_REQUEST_LIMIT 8198 +#define DMAPI5_TOPIC_REQUEST_LIMIT 65529 +#define DMAPI5_TOPIC_RESPONSE_LIMIT 65528 + #define DMAPI5_BRIDGE_COMMAND_PORT_UPDATE 0 #define DMAPI5_BRIDGE_COMMAND_STARTUP 1 #define DMAPI5_BRIDGE_COMMAND_PRIME 2 #define DMAPI5_BRIDGE_COMMAND_REBOOT 3 #define DMAPI5_BRIDGE_COMMAND_KILL 4 #define DMAPI5_BRIDGE_COMMAND_CHAT_SEND 5 +#define DMAPI5_BRIDGE_COMMAND_CHUNK 6 #define DMAPI5_PARAMETER_ACCESS_IDENTIFIER "accessIdentifier" #define DMAPI5_PARAMETER_CUSTOM_COMMANDS "customCommands" +#define DMAPI5_CHUNK "chunk" +#define DMAPI5_CHUNK_PAYLOAD "payload" +#define DMAPI5_CHUNK_TOTAL "totalChunks" +#define DMAPI5_CHUNK_SEQUENCE_ID "sequenceId" +#define DMAPI5_CHUNK_PAYLOAD_ID "payloadId" + +#define DMAPI5_MISSING_CHUNKS "missingChunks" + #define DMAPI5_RESPONSE_ERROR_MESSAGE "errorMessage" #define DMAPI5_BRIDGE_PARAMETER_COMMAND_TYPE "commandType" @@ -64,6 +77,8 @@ #define DMAPI5_TOPIC_COMMAND_SERVER_PORT_UPDATE 6 #define DMAPI5_TOPIC_COMMAND_HEARTBEAT 7 #define DMAPI5_TOPIC_COMMAND_WATCHDOG_REATTACH 8 +#define DMAPI5_TOPIC_COMMAND_SEND_CHUNK 9 +#define DMAPI5_TOPIC_COMMAND_RECEIVE_CHUNK 10 #define DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE "commandType" #define DMAPI5_TOPIC_PARAMETER_CHAT_COMMAND "chatCommand" diff --git a/code/modules/tgs/v5/api.dm b/code/modules/tgs/v5/api.dm index 0e75b0de1c..517240f12f 100644 --- a/code/modules/tgs/v5/api.dm +++ b/code/modules/tgs/v5/api.dm @@ -17,6 +17,11 @@ var/initialized = FALSE + var/chunked_requests = 0 + var/list/chunked_topics = list() + + var/detached = FALSE + /datum/tgs_api/v5/ApiVersion() return new /datum/tgs_version( #include "__interop_version.dm" @@ -97,13 +102,6 @@ /datum/tgs_api/v5/OnInitializationComplete() Bridge(DMAPI5_BRIDGE_COMMAND_PRIME) -/datum/tgs_api/v5/proc/TopicResponse(error_message = null) - var/list/response = list() - if(error_message) - response[DMAPI5_RESPONSE_ERROR_MESSAGE] = error_message - return json_encode(response) - return "{}" - /datum/tgs_api/v5/OnTopic(T) RequireInitialBridgeResponse() var/list/params = params2list(T) @@ -111,163 +109,11 @@ if(!json) return FALSE // continue to /world/Topic - var/list/topic_parameters = json_decode(json) - if(!topic_parameters) - return TopicResponse("Invalid topic parameters json!"); - if(!initialized) - TGS_WARNING_LOG("Missed topic due to not being initialized: [T]") + TGS_WARNING_LOG("Missed topic due to not being initialized: [json]") return TRUE // too early to handle, but it's still our responsibility - var/their_sCK = topic_parameters[DMAPI5_PARAMETER_ACCESS_IDENTIFIER] - if(their_sCK != access_identifier) - return TopicResponse("Failed to decode [DMAPI5_PARAMETER_ACCESS_IDENTIFIER] from: [json]!"); - - var/command = topic_parameters[DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE] - if(!isnum(command)) - return TopicResponse("Failed to decode [DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE] from: [json]!") - - switch(command) - if(DMAPI5_TOPIC_COMMAND_CHAT_COMMAND) - intercepted_message_queue = list() - var/result = HandleCustomCommand(topic_parameters[DMAPI5_TOPIC_PARAMETER_CHAT_COMMAND]) - if(!result) - result = TopicResponse("Error running chat command!") - result[DMAPI5_TOPIC_RESPONSE_CHAT_RESPONSES] = intercepted_message_queue - intercepted_message_queue = null - return result - if(DMAPI5_TOPIC_COMMAND_EVENT_NOTIFICATION) - intercepted_message_queue = list() - var/list/event_notification = topic_parameters[DMAPI5_TOPIC_PARAMETER_EVENT_NOTIFICATION] - if(!istype(event_notification)) - return TopicResponse("Invalid [DMAPI5_TOPIC_PARAMETER_EVENT_NOTIFICATION]!") - - var/event_type = event_notification[DMAPI5_EVENT_NOTIFICATION_TYPE] - if(!isnum(event_type)) - return TopicResponse("Invalid or missing [DMAPI5_EVENT_NOTIFICATION_TYPE]!") - - var/list/event_parameters = event_notification[DMAPI5_EVENT_NOTIFICATION_PARAMETERS] - if(event_parameters && !istype(event_parameters)) - return TopicResponse("Invalid or missing [DMAPI5_EVENT_NOTIFICATION_PARAMETERS]!") - - var/list/event_call = list(event_type) - if(event_parameters) - event_call += event_parameters - - if(event_handler != null) - event_handler.HandleEvent(arglist(event_call)) - - var/list/response = list() - response[DMAPI5_TOPIC_RESPONSE_CHAT_RESPONSES] = intercepted_message_queue - intercepted_message_queue = null - return json_encode(response) - if(DMAPI5_TOPIC_COMMAND_CHANGE_PORT) - var/new_port = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_PORT] - if (!isnum(new_port) || !(new_port > 0)) - return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_PORT]]") - - if(event_handler != null) - event_handler.HandleEvent(TGS_EVENT_PORT_SWAP, new_port) - - //the topic still completes, miraculously - //I honestly didn't believe byond could do it without exploding - if(!world.OpenPort(new_port)) - return TopicResponse("Port change failed!") - - return TopicResponse() - if(DMAPI5_TOPIC_COMMAND_CHANGE_REBOOT_STATE) - var/new_reboot_mode = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_REBOOT_STATE] - if(!isnum(new_reboot_mode)) - return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_REBOOT_STATE]!") - - if(event_handler != null) - event_handler.HandleEvent(TGS_EVENT_REBOOT_MODE_CHANGE, reboot_mode, new_reboot_mode) - - reboot_mode = new_reboot_mode - return TopicResponse() - if(DMAPI5_TOPIC_COMMAND_INSTANCE_RENAMED) - var/new_instance_name = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_INSTANCE_NAME] - if(!istext(new_instance_name)) - return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_INSTANCE_NAME]!") - - if(event_handler != null) - event_handler.HandleEvent(TGS_EVENT_INSTANCE_RENAMED, new_instance_name) - - instance_name = new_instance_name - return TopicResponse() - if(DMAPI5_TOPIC_COMMAND_CHAT_CHANNELS_UPDATE) - var/list/chat_update_json = topic_parameters[DMAPI5_TOPIC_PARAMETER_CHAT_UPDATE] - if(!istype(chat_update_json)) - return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_CHAT_UPDATE]!") - - DecodeChannels(chat_update_json) - return TopicResponse() - if(DMAPI5_TOPIC_COMMAND_SERVER_PORT_UPDATE) - var/new_port = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_PORT] - if (!isnum(new_port) || !(new_port > 0)) - return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_PORT]]") - - server_port = new_port - return TopicResponse() - if(DMAPI5_TOPIC_COMMAND_HEARTBEAT) - return TopicResponse() - if(DMAPI5_TOPIC_COMMAND_WATCHDOG_REATTACH) - var/new_port = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_PORT] - var/error_message = null - if (new_port != null) - if (!isnum(new_port) || !(new_port > 0)) - error_message = "Invalid [DMAPI5_TOPIC_PARAMETER_NEW_PORT]]" - else - server_port = new_port - - var/new_version_string = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_SERVER_VERSION] - if (!istext(new_version_string)) - if(error_message != null) - error_message += ", " - error_message += "Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_SERVER_VERSION]]" - else - var/datum/tgs_version/new_version = new(new_version_string) - if (event_handler) - event_handler.HandleEvent(TGS_EVENT_WATCHDOG_REATTACH, new_version) - - version = new_version - - return json_encode(list(DMAPI5_RESPONSE_ERROR_MESSAGE = error_message, DMAPI5_PARAMETER_CUSTOM_COMMANDS = ListCustomCommands())) - - return TopicResponse("Unknown command: [command]") - -/datum/tgs_api/v5/proc/Bridge(command, list/data) - if(!data) - data = list() - - data[DMAPI5_BRIDGE_PARAMETER_COMMAND_TYPE] = command - data[DMAPI5_PARAMETER_ACCESS_IDENTIFIER] = access_identifier - - var/json = json_encode(data) - var/encoded_json = url_encode(json) - - // This is an infinite sleep until we get a response - var/export_response = world.Export("http://127.0.0.1:[server_port]/Bridge?[DMAPI5_BRIDGE_DATA]=[encoded_json]") - if(!export_response) - TGS_ERROR_LOG("Failed export request: [json]") - return - - var/response_json = file2text(export_response["CONTENT"]) - if(!response_json) - TGS_ERROR_LOG("Failed export request, missing content!") - return - - var/list/bridge_response = json_decode(response_json) - if(!bridge_response) - TGS_ERROR_LOG("Failed export request, bad json: [response_json]") - return - - var/error = bridge_response[DMAPI5_RESPONSE_ERROR_MESSAGE] - if(error) - TGS_ERROR_LOG("Failed export request, bad request: [error]") - return - - return bridge_response + return ProcessTopicJson(json, TRUE) /datum/tgs_api/v5/OnReboot() var/list/result = Bridge(DMAPI5_BRIDGE_COMMAND_REBOOT) diff --git a/code/modules/tgs/v5/bridge.dm b/code/modules/tgs/v5/bridge.dm new file mode 100644 index 0000000000..b3cf775939 --- /dev/null +++ b/code/modules/tgs/v5/bridge.dm @@ -0,0 +1,95 @@ +/datum/tgs_api/v5/proc/Bridge(command, list/data) + if(!data) + data = list() + + var/single_bridge_request = CreateBridgeRequest(command, data) + if(length(single_bridge_request) <= DMAPI5_BRIDGE_REQUEST_LIMIT) + return PerformBridgeRequest(single_bridge_request) + + // chunking required + var/payload_id = ++chunked_requests + + var/raw_data = CreateBridgeData(command, data, FALSE) + + var/list/chunk_requests = GenerateChunks(raw_data, TRUE) + + var/list/response + for(var/bridge_request in chunk_requests) + response = PerformBridgeRequest(bridge_request) + if(!response) + // Abort + return + + var/list/missing_sequence_ids = response[DMAPI5_MISSING_CHUNKS] + if(length(missing_sequence_ids)) + do + TGS_WARNING_LOG("Server is still missing some chunks of bridge P[payload_id]! Sending missing chunks...") + if(!istype(missing_sequence_ids)) + TGS_ERROR_LOG("Did not receive a list() for [DMAPI5_MISSING_CHUNKS]!") + return + + for(var/missing_sequence_id in missing_sequence_ids) + if(!isnum(missing_sequence_id)) + TGS_ERROR_LOG("Did not receive a num in [DMAPI5_MISSING_CHUNKS]!") + return + + var/missing_chunk_request = chunk_requests[missing_sequence_id + 1] + response = PerformBridgeRequest(missing_chunk_request) + if(!response) + // Abort + return + + missing_sequence_ids = response[DMAPI5_MISSING_CHUNKS] + while(length(missing_sequence_ids)) + + return response + +/datum/tgs_api/v5/proc/CreateBridgeRequest(command, list/data) + var/json = CreateBridgeData(command, data, TRUE) + var/encoded_json = url_encode(json) + + var/url = "http://127.0.0.1:[server_port]/Bridge?[DMAPI5_BRIDGE_DATA]=[encoded_json]" + return url + +/datum/tgs_api/v5/proc/CreateBridgeData(command, list/data, needs_auth) + data[DMAPI5_BRIDGE_PARAMETER_COMMAND_TYPE] = command + if(needs_auth) + data[DMAPI5_PARAMETER_ACCESS_IDENTIFIER] = access_identifier + + var/json = json_encode(data) + return json + +/datum/tgs_api/v5/proc/PerformBridgeRequest(bridge_request) + if(detached) + // Wait up to one minute + for(var/i in 1 to 600) + sleep(1) + if(!detached) + break + + // dad went out for milk cigarettes 20 years ago... + if(i == 600) + detached = FALSE + + // This is an infinite sleep until we get a response + var/export_response = world.Export(bridge_request) + if(!export_response) + TGS_ERROR_LOG("Failed bridge request: [bridge_request]") + return + + var/response_json = file2text(export_response["CONTENT"]) + if(!response_json) + TGS_ERROR_LOG("Failed bridge request, missing content!") + return + + var/list/bridge_response = json_decode(response_json) + if(!bridge_response) + TGS_ERROR_LOG("Failed bridge request, bad json: [response_json]") + return + + var/error = bridge_response[DMAPI5_RESPONSE_ERROR_MESSAGE] + if(error) + TGS_ERROR_LOG("Failed bridge request, bad request: [error]") + return + + return bridge_response diff --git a/code/modules/tgs/v5/chunking.dm b/code/modules/tgs/v5/chunking.dm new file mode 100644 index 0000000000..cd5944d34f --- /dev/null +++ b/code/modules/tgs/v5/chunking.dm @@ -0,0 +1,43 @@ +/datum/tgs_api/v5/proc/GenerateChunks(payload, bridge) + var/limit = bridge ? DMAPI5_BRIDGE_REQUEST_LIMIT : DMAPI5_TOPIC_RESPONSE_LIMIT + + var/payload_id = ++chunked_requests + var/data_length = length(payload) + + var/chunk_count + var/list/chunk_requests + for(chunk_count = 2; !chunk_requests; ++chunk_count) + var/max_chunk_size = -round(-(data_length / chunk_count)) + if(max_chunk_size > limit) + continue + + chunk_requests = list() + for(var/i in 1 to chunk_count) + var/start_index = 1 + ((i - 1) * max_chunk_size) + if (start_index > data_length) + break + + var/end_index = min(1 + (i * max_chunk_size), data_length + 1) + + var/chunk_payload = copytext(payload, start_index, end_index) + + // sequence IDs in interop chunking are always zero indexed + var/list/chunk = list(DMAPI5_CHUNK_PAYLOAD_ID = payload_id, DMAPI5_CHUNK_SEQUENCE_ID = (i - 1), DMAPI5_CHUNK_TOTAL = chunk_count, DMAPI5_CHUNK_PAYLOAD = chunk_payload) + + var/chunk_request = list(DMAPI5_CHUNK = chunk) + var/chunk_length + if(bridge) + chunk_request = CreateBridgeRequest(DMAPI5_BRIDGE_COMMAND_CHUNK, chunk_request) + chunk_length = length(chunk_request) + else + chunk_request = list(chunk_request) // wrap for adding to list + chunk_length = length(json_encode(chunk_request)) + + if(chunk_length > limit) + // Screwed by encoding, no way to preempt it though + chunk_requests = null + break + + chunk_requests += chunk_request + + return chunk_requests diff --git a/code/modules/tgs/v5/commands.dm b/code/modules/tgs/v5/commands.dm index 71ede42c3b..a832c81f17 100644 --- a/code/modules/tgs/v5/commands.dm +++ b/code/modules/tgs/v5/commands.dm @@ -36,10 +36,10 @@ var/datum/tgs_message_content/response = sc.Run(u, params) response = UpgradeDeprecatedCommandResponse(response, command) - var/list/topic_response = list() + var/list/topic_response = TopicResponse() topic_response[DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE_MESSAGE] = response?.text topic_response[DMAPI5_TOPIC_RESPONSE_COMMAND_RESPONSE] = response?._interop_serialize() - return json_encode(topic_response) + return topic_response return TopicResponse("Unknown custom chat command: [command]!") // Common proc b/c it's used by the V3/V4 APIs diff --git a/code/modules/tgs/v5/topic.dm b/code/modules/tgs/v5/topic.dm new file mode 100644 index 0000000000..28fcc14aef --- /dev/null +++ b/code/modules/tgs/v5/topic.dm @@ -0,0 +1,258 @@ +/datum/tgs_api/v5/proc/TopicResponse(error_message = null) + var/list/response = list() + if(error_message) + response[DMAPI5_RESPONSE_ERROR_MESSAGE] = error_message + return response + +/datum/tgs_api/v5/proc/ProcessTopicJson(json, check_access_identifier) + var/list/result = ProcessRawTopic(json, check_access_identifier) + if(!result) + result = TopicResponse("Runtime error!") + else if(!length(result)) + return "{}" // quirk of json_encode is an empty list returns "[]" + + var/response_json = json_encode(result) + if(length(response_json) > DMAPI5_TOPIC_RESPONSE_LIMIT) + // cache response chunks and send the first + var/list/chunks = GenerateChunks(response_json, FALSE) + var/payload_id = chunks[1][DMAPI5_CHUNK][DMAPI5_CHUNK_PAYLOAD_ID] + var/cache_key = ResponseTopicChunkCacheKey(payload_id) + + chunked_topics[cache_key] = chunks + + response_json = json_encode(chunks[1]) + + return response_json + +/datum/tgs_api/v5/proc/ProcessRawTopic(json, check_access_identifier) + var/list/topic_parameters = json_decode(json) + if(!topic_parameters) + return TopicResponse("Invalid topic parameters json: [json]!"); + + var/their_sCK = topic_parameters[DMAPI5_PARAMETER_ACCESS_IDENTIFIER] + if(check_access_identifier && their_sCK != access_identifier) + return TopicResponse("Failed to decode [DMAPI5_PARAMETER_ACCESS_IDENTIFIER]!") + + var/command = topic_parameters[DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE] + if(!isnum(command)) + return TopicResponse("Failed to decode [DMAPI5_TOPIC_PARAMETER_COMMAND_TYPE]!") + + return ProcessTopicCommand(command, topic_parameters) + +/datum/tgs_api/v5/proc/ResponseTopicChunkCacheKey(payload_id) + return "response[payload_id]" + +/datum/tgs_api/v5/proc/ProcessTopicCommand(command, list/topic_parameters) + switch(command) + + if(DMAPI5_TOPIC_COMMAND_CHAT_COMMAND) + intercepted_message_queue = list() + var/list/result = HandleCustomCommand(topic_parameters[DMAPI5_TOPIC_PARAMETER_CHAT_COMMAND]) + if(!result) + result = TopicResponse("Error running chat command!") + result[DMAPI5_TOPIC_RESPONSE_CHAT_RESPONSES] = intercepted_message_queue + intercepted_message_queue = null + return result + + if(DMAPI5_TOPIC_COMMAND_EVENT_NOTIFICATION) + intercepted_message_queue = list() + var/list/event_notification = topic_parameters[DMAPI5_TOPIC_PARAMETER_EVENT_NOTIFICATION] + if(!istype(event_notification)) + return TopicResponse("Invalid [DMAPI5_TOPIC_PARAMETER_EVENT_NOTIFICATION]!") + + var/event_type = event_notification[DMAPI5_EVENT_NOTIFICATION_TYPE] + if(!isnum(event_type)) + return TopicResponse("Invalid or missing [DMAPI5_EVENT_NOTIFICATION_TYPE]!") + + var/list/event_parameters = event_notification[DMAPI5_EVENT_NOTIFICATION_PARAMETERS] + if(event_parameters && !istype(event_parameters)) + return TopicResponse("Invalid or missing [DMAPI5_EVENT_NOTIFICATION_PARAMETERS]!") + + var/list/event_call = list(event_type) + if (event_type == TGS_EVENT_WATCHDOG_DETACH) + detached = TRUE + + if(event_parameters) + event_call += event_parameters + + if(event_handler != null) + event_handler.HandleEvent(arglist(event_call)) + + var/list/response = TopicResponse() + response[DMAPI5_TOPIC_RESPONSE_CHAT_RESPONSES] = intercepted_message_queue + intercepted_message_queue = null + return response + + if(DMAPI5_TOPIC_COMMAND_CHANGE_PORT) + var/new_port = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_PORT] + if (!isnum(new_port) || !(new_port > 0)) + return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_PORT]]") + + if(event_handler != null) + event_handler.HandleEvent(TGS_EVENT_PORT_SWAP, new_port) + + //the topic still completes, miraculously + //I honestly didn't believe byond could do it without exploding + if(!world.OpenPort(new_port)) + return TopicResponse("Port change failed!") + + return TopicResponse() + + if(DMAPI5_TOPIC_COMMAND_CHANGE_REBOOT_STATE) + var/new_reboot_mode = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_REBOOT_STATE] + if(!isnum(new_reboot_mode)) + return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_REBOOT_STATE]!") + + if(event_handler != null) + event_handler.HandleEvent(TGS_EVENT_REBOOT_MODE_CHANGE, reboot_mode, new_reboot_mode) + + reboot_mode = new_reboot_mode + return TopicResponse() + + if(DMAPI5_TOPIC_COMMAND_INSTANCE_RENAMED) + var/new_instance_name = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_INSTANCE_NAME] + if(!istext(new_instance_name)) + return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_INSTANCE_NAME]!") + + if(event_handler != null) + event_handler.HandleEvent(TGS_EVENT_INSTANCE_RENAMED, new_instance_name) + + instance_name = new_instance_name + return TopicResponse() + + if(DMAPI5_TOPIC_COMMAND_CHAT_CHANNELS_UPDATE) + var/list/chat_update_json = topic_parameters[DMAPI5_TOPIC_PARAMETER_CHAT_UPDATE] + if(!istype(chat_update_json)) + return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_CHAT_UPDATE]!") + + DecodeChannels(chat_update_json) + return TopicResponse() + + if(DMAPI5_TOPIC_COMMAND_SERVER_PORT_UPDATE) + var/new_port = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_PORT] + if (!isnum(new_port) || !(new_port > 0)) + return TopicResponse("Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_PORT]]") + + server_port = new_port + return TopicResponse() + + if(DMAPI5_TOPIC_COMMAND_HEARTBEAT) + return TopicResponse() + + if(DMAPI5_TOPIC_COMMAND_WATCHDOG_REATTACH) + detached = FALSE + var/new_port = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_PORT] + var/error_message = null + if (new_port != null) + if (!isnum(new_port) || !(new_port > 0)) + error_message = "Invalid [DMAPI5_TOPIC_PARAMETER_NEW_PORT]]" + else + server_port = new_port + + var/new_version_string = topic_parameters[DMAPI5_TOPIC_PARAMETER_NEW_SERVER_VERSION] + if (!istext(new_version_string)) + if(error_message != null) + error_message += ", " + error_message += "Invalid or missing [DMAPI5_TOPIC_PARAMETER_NEW_SERVER_VERSION]]" + else + var/datum/tgs_version/new_version = new(new_version_string) + if (event_handler) + event_handler.HandleEvent(TGS_EVENT_WATCHDOG_REATTACH, new_version) + + version = new_version + + var/list/reattach_response = TopicResponse(error_message) + reattach_response[DMAPI5_PARAMETER_CUSTOM_COMMANDS] = ListCustomCommands() + return reattach_response + + if(DMAPI5_TOPIC_COMMAND_SEND_CHUNK) + var/list/chunk = topic_parameters[DMAPI5_CHUNK] + if(!istype(chunk)) + return TopicResponse("Invalid [DMAPI5_CHUNK]!") + + var/payload_id = chunk[DMAPI5_CHUNK_PAYLOAD_ID] + if(!isnum(payload_id)) + return TopicResponse("[DMAPI5_CHUNK_PAYLOAD_ID] is not a number!") + + // Always updated the highest known payload ID + chunked_requests = max(chunked_requests, payload_id) + + var/sequence_id = chunk[DMAPI5_CHUNK_SEQUENCE_ID] + if(!isnum(sequence_id)) + return TopicResponse("[DMAPI5_CHUNK_SEQUENCE_ID] is not a number!") + + var/total_chunks = chunk[DMAPI5_CHUNK_TOTAL] + if(!isnum(total_chunks)) + return TopicResponse("[DMAPI5_CHUNK_TOTAL] is not a number!") + + if(total_chunks == 0) + return TopicResponse("[DMAPI5_CHUNK_TOTAL] is zero!") + + var/payload = chunk[DMAPI5_CHUNK_PAYLOAD] + if(!istext(payload)) + return TopicResponse("[DMAPI5_CHUNK_PAYLOAD] is not text!") + + var/cache_key = "request[payload_id]" + var/payloads = chunked_topics[cache_key] + + if(!payloads) + payloads = new /list(total_chunks) + chunked_topics[cache_key] = payloads + + if(total_chunks != length(payloads)) + chunked_topics -= cache_key + return TopicResponse("Received differing total chunks for same [DMAPI5_CHUNK_PAYLOAD_ID]! Invalidating [DMAPI5_CHUNK_PAYLOAD_ID]!") + + var/pre_existing_chunk = payloads[sequence_id + 1] + if(pre_existing_chunk && pre_existing_chunk != payload) + chunked_topics -= cache_key + return TopicResponse("Received differing payload for same [DMAPI5_CHUNK_SEQUENCE_ID]! Invalidating [DMAPI5_CHUNK_PAYLOAD_ID]!") + + payloads[sequence_id + 1] = payload + + var/list/missing_sequence_ids = list() + for(var/i in 1 to total_chunks) + if(!payloads[i]) + missing_sequence_ids += i - 1 + + if(length(missing_sequence_ids)) + return list(DMAPI5_MISSING_CHUNKS = missing_sequence_ids) + + chunked_topics -= cache_key + var/full_json = jointext(payloads, "") + + return ProcessRawTopic(full_json, FALSE) + + if(DMAPI5_TOPIC_COMMAND_RECEIVE_CHUNK) + var/payload_id = topic_parameters[DMAPI5_CHUNK_PAYLOAD_ID] + if(!isnum(payload_id)) + return TopicResponse("[DMAPI5_CHUNK_PAYLOAD_ID] is not a number!") + + // Always updated the highest known payload ID + chunked_requests = max(chunked_requests, payload_id) + + var/list/missing_chunks = topic_parameters[DMAPI5_MISSING_CHUNKS] + if(!istype(missing_chunks) || !length(missing_chunks)) + return TopicResponse("Missing or empty [DMAPI5_MISSING_CHUNKS]!") + + var/sequence_id_to_send = missing_chunks[1] + if(!isnum(sequence_id_to_send)) + return TopicResponse("[DMAPI5_MISSING_CHUNKS] contained a non-number!") + + var/cache_key = ResponseTopicChunkCacheKey(payload_id) + var/list/chunks = chunked_topics[cache_key] + if(!chunks) + return TopicResponse("Unknown response chunk set: P[payload_id]!") + + // sequence IDs in interop chunking are always zero indexed + var/chunk_to_send = chunks[sequence_id_to_send + 1] + if(!chunk_to_send) + return TopicResponse("Sequence ID [sequence_id_to_send] is not present in response chunk P[payload_id]!") + + if(length(missing_chunks) == 1) + // sending last chunk, purge the cache + chunked_topics -= cache_key + + return chunk_to_send + + return TopicResponse("Unknown command: [command]") diff --git a/code/modules/tgs/v5/undefs.dm b/code/modules/tgs/v5/undefs.dm index 6209945372..e3455b69d1 100644 --- a/code/modules/tgs/v5/undefs.dm +++ b/code/modules/tgs/v5/undefs.dm @@ -4,16 +4,29 @@ #undef DMAPI5_BRIDGE_DATA #undef DMAPI5_TOPIC_DATA +#undef DMAPI5_BRIDGE_REQUEST_LIMIT +#undef DMAPI5_TOPIC_REQUEST_LIMIT +#undef DMAPI5_TOPIC_RESPONSE_LIMIT + #undef DMAPI5_BRIDGE_COMMAND_PORT_UPDATE #undef DMAPI5_BRIDGE_COMMAND_STARTUP #undef DMAPI5_BRIDGE_COMMAND_PRIME #undef DMAPI5_BRIDGE_COMMAND_REBOOT #undef DMAPI5_BRIDGE_COMMAND_KILL #undef DMAPI5_BRIDGE_COMMAND_CHAT_SEND +#undef DMAPI5_BRIDGE_COMMAND_CHUNK #undef DMAPI5_PARAMETER_ACCESS_IDENTIFIER #undef DMAPI5_PARAMETER_CUSTOM_COMMANDS +#undef DMAPI5_CHUNK +#undef DMAPI5_CHUNK_PAYLOAD +#undef DMAPI5_CHUNK_TOTAL +#undef DMAPI5_CHUNK_SEQUENCE_ID +#undef DMAPI5_CHUNK_PAYLOAD_ID + +#undef DMAPI5_MISSING_CHUNKS + #undef DMAPI5_RESPONSE_ERROR_MESSAGE #undef DMAPI5_BRIDGE_PARAMETER_COMMAND_TYPE diff --git a/config/splurt/general.txt b/config/splurt/general.txt index b42b8e7234..53c680f96b 100644 --- a/config/splurt/general.txt +++ b/config/splurt/general.txt @@ -22,3 +22,7 @@ SM_DELAMINATION # Protolathe access # Comment to make protolathes and mechfabs use their access locks PROTOLOCK_ALL_ACCESS + +# Character color limits +# Uncomment to disallow players from making characters with colors that are too dark +CHARACTER_COLOR_LIMITS diff --git a/html/changelogs/archive/2023-04.yml b/html/changelogs/archive/2023-04.yml index 71ff37a767..22b35133cd 100644 --- a/html/changelogs/archive/2023-04.yml +++ b/html/changelogs/archive/2023-04.yml @@ -3,3 +3,35 @@ - rscadd: Adds a lot of new suits - bugfix: fixes the tuxedo sprites - bugfix: fixes the double wedding crate +2023-04-06: + SandPoot: + - tweak: Pointing will make the arrow face it and you can also pixel-perfect point. +2023-04-08: + Anonymous: + - rscadd: Leashes are now available in BarkBox as well. +2023-04-09: + Anonymous: + - imageadd: 'Emojis: dawgdoin, ackshually, pain.' + CreepingRose: + - rscadd: Added Ultimate Spray Bottle + - rscadd: Added Alien Janitoring research node, requires Advanced Janitoring and + Alien Stock Parts to research for 1500 points + - imageadd: added Ultimate Spray Bottle's .dmi as janitor.dmi in modular_splurt +2023-04-12: + Anonymous: + - soundadd: 'Emotes: \*whistle_tune, \*terror.' +2023-04-18: + KeplerWasTaken: + - rscdel: Removes fascist symbols +2023-04-23: + Azarak: + - rscadd: Added new body morpher quirk, which allows people to morph like slimepeople + BongaTheProto: + - config: Makes character color limits a config (so you can make darker characters + now) + - tweak: select full black for default species color, select it again for full black + Pin-alternative: + - tweak: The genital manager is now its own menu you can enter through the right + click options + - bugfix: genitals now properly show how full of fluids they are + - code_imp: Made breasts a not so snowflake to handle diff --git a/icons/effects/crayondecal.dmi b/icons/effects/crayondecal.dmi index 5e848ec400..10c2e3353a 100644 Binary files a/icons/effects/crayondecal.dmi and b/icons/effects/crayondecal.dmi differ diff --git a/icons/emoji.dmi b/icons/emoji.dmi index 290951bcab..a784a80dc1 100644 Binary files a/icons/emoji.dmi and b/icons/emoji.dmi differ diff --git a/icons/emoji_32.dmi b/icons/emoji_32.dmi index 1a0684c2fd..5da4a28f22 100644 Binary files a/icons/emoji_32.dmi and b/icons/emoji_32.dmi differ diff --git a/modular_citadel/code/modules/client/preferences_savefile.dm b/modular_citadel/code/modules/client/preferences_savefile.dm index af527a50e5..8d2bc84a2a 100644 --- a/modular_citadel/code/modules/client/preferences_savefile.dm +++ b/modular_citadel/code/modules/client/preferences_savefile.dm @@ -7,9 +7,9 @@ features["ipc_antenna"] = sanitize_inlist(features["ipc_antenna"], GLOB.ipc_antennas_list) //Citadel features["flavor_text"] = sanitize_text(features["flavor_text"], initial(features["flavor_text"])) - if(!features["mcolor2"] || features["mcolor"] == "#000000") + if(!features["mcolor2"] || (features["mcolor"] == "#000000" && CONFIG_GET(flag/character_color_limits))) //SPLURT EDIT features["mcolor2"] = pick("FFFFFF","7F7F7F", "7FFF7F", "7F7FFF", "FF7F7F", "7FFFFF", "FF7FFF", "FFFF7F") - if(!features["mcolor3"] || features["mcolor"] == "#000000") + if(!features["mcolor3"] || (features["mcolor"] == "#000000" && CONFIG_GET(flag/character_color_limits))) //SPLURT EDIT features["mcolor3"] = pick("FFFFFF","7F7F7F", "7FFF7F", "7F7FFF", "FF7F7F", "7FFFFF", "FF7FFF", "FFFF7F") features["mcolor2"] = sanitize_hexcolor(features["mcolor2"], 6, FALSE) features["mcolor3"] = sanitize_hexcolor(features["mcolor3"], 6, FALSE) diff --git a/modular_citadel/code/modules/reagents/chemistry/reagents/enlargement.dm b/modular_citadel/code/modules/reagents/chemistry/reagents/enlargement.dm index 24b0fc0a76..8f0e3d00d7 100644 --- a/modular_citadel/code/modules/reagents/chemistry/reagents/enlargement.dm +++ b/modular_citadel/code/modules/reagents/chemistry/reagents/enlargement.dm @@ -68,8 +68,7 @@ B.color = "#[M.dna.features["breasts_color"]]" else B.color = SKINTONE2HEX(H.skin_tone) - B.size = "flat" - B.cached_size = 0 + B.size = 0 B.prev_size = 0 to_chat(H, "Your chest feels warm, tingling with newfound sensitivity.") H.reagents.remove_reagent(type, 5) @@ -145,12 +144,12 @@ var/obj/item/organ/genital/breasts/B = M.getorganslot(ORGAN_SLOT_BREASTS) if(!B) return..() - var/optimal_size = B.breast_values[M.dna.features["breasts_size"]] + var/optimal_size = GLOB.breast_values[M.dna.features["breasts_size"]] if(!optimal_size)//Fast fix for those who don't want it. B.modify_size(-0.1) - else if(B.cached_size > optimal_size) + else if(B.size > optimal_size) B.modify_size(-0.05, optimal_size) - else if(B.cached_size < optimal_size) + else if(B.size < optimal_size) B.modify_size(0.05, 0, optimal_size) return ..() @@ -238,7 +237,7 @@ if(B) B.modify_size(-0.05) - if(V && (!B || B.cached_size <= 0)) + if(V && (!B || B.size <= 0)) qdel(V) if(W && (!B || B.size <= 0)) qdel(W) diff --git a/modular_sand/code/datums/components/interaction_menu_granter.dm b/modular_sand/code/datums/components/interaction_menu_granter.dm index b4eed52520..9f4fab4a7a 100644 --- a/modular_sand/code/datums/components/interaction_menu_granter.dm +++ b/modular_sand/code/datums/components/interaction_menu_granter.dm @@ -136,14 +136,8 @@ else visibility = "Hidden by clothes" - var/extras = "None" - if(CHECK_BITFIELD(genital.genital_flags, GENITAL_CAN_STUFF)) - extras = "Allows egg stuffing" - - genital_entry["extras"] = extras genital_entry["visibility"] = visibility genital_entry["possible_choices"] = GLOB.genitals_visibility_toggles - genital_entry["extra_choices"] = list(GEN_ALLOW_EGG_STUFFING) genital_entry["can_arouse"] = ( !!CHECK_BITFIELD(genital.genital_flags, GENITAL_CAN_AROUSE) \ && !(HAS_TRAIT(get_genitals, TRAIT_PERMABONER) \ @@ -169,37 +163,6 @@ genitals += list(simulated_ass) .["genitals"] = genitals - //Get their genitals - var/list/genital_fluids = list() - var/mob/living/carbon/target_genitals = target || self - if(istype(target_genitals)) - for(var/obj/item/organ/genital/genital in target_genitals.internal_organs) - if(!(CHECK_BITFIELD(genital.genital_flags, GENITAL_FUID_PRODUCTION))) - continue - var/fluids = (clamp(genital.fluid_rate * ((world.time - genital.last_orgasmed) / (10 SECONDS)) * genital.fluid_mult, 0, genital.fluid_max_volume) / genital.fluid_max_volume) - var/list/genital_entry = list() - genital_entry["name"] = "[genital.name]" - genital_entry["key"] = REF(genital) - genital_entry["fluid"] = fluids - genital_fluids += list(genital_entry) - .["genital_fluids"] = genital_fluids - - var/list/genital_interactibles = list() - if(istype(target_genitals)) - for(var/obj/item/organ/genital/genital in target_genitals.internal_organs) - if(!genital.is_exposed()) - continue - var/list/equipment_names = list() - for(var/obj/equipment in genital.contents) - equipment_names += equipment.name - var/list/genital_entry = list() - genital_entry["name"] = "[genital.name]" - genital_entry["key"] = REF(genital) - genital_entry["possible_choices"] = GLOB.genitals_interactions - genital_entry["equipments"] = equipment_names - genital_interactibles += list(genital_entry) - .["genital_interactibles"] = genital_interactibles - var/datum/preferences/prefs = self?.client.prefs if(prefs) //Getting char prefs diff --git a/modular_sand/code/datums/interactions/interaction_datums/lewd/breasts.dm b/modular_sand/code/datums/interactions/interaction_datums/lewd/breasts.dm index a8e4b5bc44..5f4c1354ba 100644 --- a/modular_sand/code/datums/interactions/interaction_datums/lewd/breasts.dm +++ b/modular_sand/code/datums/interactions/interaction_datums/lewd/breasts.dm @@ -41,8 +41,8 @@ if("f", "g", "h") modifier = 3 else - if(milkers.size in milkers.breast_values) - modifier = clamp(milkers.breast_values[milkers.size] - 5, 0, INFINITY) + if(milkers.size in GLOB.breast_values) + modifier = clamp(GLOB.breast_values[milkers.size] - 5, 0, INFINITY) else modifier = 1 target.reagents.add_reagent(milktype, rand(1,3 * modifier)) diff --git a/modular_sand/code/datums/interactions/interaction_datums/lewd/nipsuck.dm b/modular_sand/code/datums/interactions/interaction_datums/lewd/nipsuck.dm index 6d9ccc511d..b7301ceec9 100644 --- a/modular_sand/code/datums/interactions/interaction_datums/lewd/nipsuck.dm +++ b/modular_sand/code/datums/interactions/interaction_datums/lewd/nipsuck.dm @@ -22,8 +22,8 @@ if("f", "g", "h") modifier = 3 else - if(B.size in B.breast_values) - modifier = clamp(B.breast_values[B.size] - 5, 0, INFINITY) + if(B.size in GLOB.breast_values) + modifier = clamp(GLOB.breast_values[B.size] - 5, 0, INFINITY) else modifier = 1 if(B.fluid_id) @@ -42,8 +42,8 @@ if("f", "g", "h") modifier = 3 else - if(B.size in B.breast_values) - modifier = clamp(B.breast_values[B.size] - 5, 0, INFINITY) + if(B.size in GLOB.breast_values) + modifier = clamp(GLOB.breast_values[B.size] - 5, 0, INFINITY) else modifier = 1 if(B.fluid_id) @@ -63,8 +63,8 @@ if("f", "g", "h") modifier = 3 else - if(B.size in B.breast_values) - modifier = clamp(B.breast_values[B.size] - 5, 0, INFINITY) + if(B.size in GLOB.breast_values) + modifier = clamp(GLOB.breast_values[B.size] - 5, 0, INFINITY) else modifier = 1 if(B.fluid_id) diff --git a/modular_sand/code/datums/interactions/interaction_datums/lewd/self/breasts.dm b/modular_sand/code/datums/interactions/interaction_datums/lewd/self/breasts.dm index 166066347e..7eeb453ca0 100644 --- a/modular_sand/code/datums/interactions/interaction_datums/lewd/self/breasts.dm +++ b/modular_sand/code/datums/interactions/interaction_datums/lewd/self/breasts.dm @@ -79,8 +79,8 @@ if("f", "g", "h") modifier = 3 else - if(milkers.size in milkers.breast_values) - modifier = clamp(milkers.breast_values[milkers.size] - 5, 0, INFINITY) + if(milkers.size in GLOB.breast_values) + modifier = clamp(GLOB.breast_values[milkers.size] - 5, 0, INFINITY) else modifier = 1 user.reagents.add_reagent(milktype, rand(1,3 * modifier) * user.get_fluid_mod(milkers), milkers) //SPLURT edit diff --git a/modular_sand/code/modules/clothing/gloves/_gloves.dm b/modular_sand/code/modules/clothing/gloves/_gloves.dm index 092e25511d..7b21d86c73 100644 --- a/modular_sand/code/modules/clothing/gloves/_gloves.dm +++ b/modular_sand/code/modules/clothing/gloves/_gloves.dm @@ -4,6 +4,10 @@ var/list/obj/item/clothing/accessory/ring/attached_accessories = list() var/list/mutable_appearance/accessory_overlays = list() +/obj/item/clothing/gloves/Destroy() + QDEL_LIST(attached_accessories) + return ..() + /obj/item/clothing/gloves/worn_overlays(isinhands = FALSE, icon_file, used_state, style_flags = NONE) . = ..() if(!isinhands) diff --git a/modular_sand/code/modules/clothing/under/_under.dm b/modular_sand/code/modules/clothing/under/_under.dm index 7542a0c85a..a1f03e2dbd 100644 --- a/modular_sand/code/modules/clothing/under/_under.dm +++ b/modular_sand/code/modules/clothing/under/_under.dm @@ -2,3 +2,7 @@ . = ..() if(!is_type_in_typecache(type, GLOB.skirt_peekable) && findlasttext("[type]", "skirt")) LAZYSET(GLOB.skirt_peekable, type, TRUE) + +/obj/item/clothing/under/Destroy() + QDEL_LIST(attached_accessories) + return ..() diff --git a/modular_sand/code/modules/mob/living/silicon/robot/robot_modules.dm b/modular_sand/code/modules/mob/living/silicon/robot/robot_modules.dm index 2bbb9f4c7d..c52935fbd4 100644 --- a/modular_sand/code/modules/mob/living/silicon/robot/robot_modules.dm +++ b/modular_sand/code/modules/mob/living/silicon/robot/robot_modules.dm @@ -88,11 +88,6 @@ basic_modules += /obj/item/reagent_containers/borghypo/borgshaker/sodashaker basic_modules += /obj/item/reagent_containers/borghypo/borgshaker/miscshaker . = ..() -/* -/obj/item/robot_module/miner/Initialize(mapload) - basic_modules += /obj/item/card/id/miningborg - . = ..() -*/ //Sandstorm, the main citacode already gives a mining points card to the minerborg, borgs don't need another one. /datum/robot_energy_storage/plasma name = "Plasma Buffer Container" diff --git a/modular_splurt/code/_globalvars/lists/global_lewd.dm b/modular_splurt/code/_globalvars/lists/global_lewd.dm index 77b051c103..7883a8ecb4 100644 --- a/modular_splurt/code/_globalvars/lists/global_lewd.dm +++ b/modular_splurt/code/_globalvars/lists/global_lewd.dm @@ -15,3 +15,5 @@ GLOBAL_LIST_INIT(breast_nouns, list("breasts", "boobs", "honkers", "tatas", "tid GLOBAL_LIST_INIT(balls_nouns, list("balls", "nuts", "ballsack", "testicles", "sack", "cum orbs", "cum spheres", "cum tanks", "cum holders", "cum churners", "spunk orbs", "spunk spheres", "spunk tanks", "spunk holders", "spunk churners", "nut orbs", "nut spheres", "nut tanks", "nut holders", "nut churners", "jizz orbs", "jizz spheres", "jizz tanks", "jizz holders", "jizz churners", "seed orbs", "seed spheres", "seed tanks", "seed holders", "seed churners")) GLOBAL_LIST_INIT(butt_nouns, list("ass", "butt", "dumptruck", "tush", "badonk", "booty", "rump", "behind")) + +GLOBAL_LIST_INIT(breast_values, list("a" = 1, "b" = 2, "c" = 3, "d" = 4, "e" = 5, "f" = 6, "g" = 7, "h" = 8, "i" = 9, "j" = 10, "k" = 11, "l" = 12, "m" = 13, "n" = 14, "o" = 15, "huge" = 16, "massive" = 17, "giga" = 25, "impossible" = 30, "flat" = 0)) diff --git a/modular_splurt/code/controllers/configuration/entries/splurt_general.dm b/modular_splurt/code/controllers/configuration/entries/splurt_general.dm index f73ee4a324..4b8655c76c 100644 --- a/modular_splurt/code/controllers/configuration/entries/splurt_general.dm +++ b/modular_splurt/code/controllers/configuration/entries/splurt_general.dm @@ -11,3 +11,5 @@ /datum/config_entry/flag/sm_delamination /datum/config_entry/flag/protolock_all_access + +/datum/config_entry/flag/character_color_limits diff --git a/modular_splurt/code/datums/genitals/genitals_interface.dm b/modular_splurt/code/datums/genitals/genitals_interface.dm new file mode 100644 index 0000000000..86cced33e9 --- /dev/null +++ b/modular_splurt/code/datums/genitals/genitals_interface.dm @@ -0,0 +1,213 @@ +/// Attempts to open the tgui menu +/mob/living/verb/genital_menu() + set name = "Genitals Menu" + set desc = "Manage your genital, or someone else's." + set category = "IC" + set src in view(usr.client) + + if(!iscarbon(usr)) + return + if(!usr.mind) //Mindless boys, honestly just don't, it's better this way + return + if(!usr.mind.genitals_menu_holder) + usr.mind.genitals_menu_holder= new(usr.mind) + + usr.mind.genitals_menu_holder.target = src + usr.mind.genitals_menu_holder.ui_interact(usr) + +/datum/mind + var/datum/genitals_menu/genitals_menu_holder + +/datum/mind/New(key) + . = ..() + genitals_menu_holder = new(src) + +/datum/genitals_menu + var/mob/living/carbon/target + +/datum/genitals_menu/ui_state(mob/user) + return GLOB.conscious_state + +/datum/genitals_menu/ui_interact(mob/user, datum/tgui/ui) + ui = SStgui.try_update_ui(user, src, ui) + if(!ui) + ui = new(user, src, "GenitalConfig", "Genitals") + ui.open() + +/datum/genitals_menu/ui_data(mob/user) + . = ..() + var/mob/living/carbon/genital_holder = target || user + var/user_is_target = genital_holder == user + .["istargetself"] = user_is_target + if(!user_is_target) + .["target_name"] = genital_holder.name + var/list/genitals = list() + for(var/obj/item/organ/genital/genital in genital_holder.internal_organs) //Only get the genitals + if(CHECK_BITFIELD(genital.genital_flags, GENITAL_INTERNAL)) //Not those though + continue + + var/list/genital_entry = list() + genital_entry["img"] = icon2base64(getFlatIcon(genital, no_anim=TRUE)) + genital_entry["name"] = "[capitalize(genital.name)]" //Prevents code from adding a prefix + genital_entry["key"] = REF(genital) //The key is the reference to the object + + genital_entry["description"] = genital.desc + "\n [genital.linked_organ ? "Linked organ: [genital.linked_organ.name]" : ""]" + + if(user_is_target) + var/visibility = "Invalid" + if(CHECK_BITFIELD(genital.genital_flags, GENITAL_THROUGH_CLOTHES)) + visibility = "Always visible" + else if(CHECK_BITFIELD(genital.genital_flags, GENITAL_UNDIES_HIDDEN)) + visibility = "Hidden by underwear" + else if(CHECK_BITFIELD(genital.genital_flags, GENITAL_HIDDEN)) + visibility = "Always hidden" + else + visibility = "Hidden by clothes" + + var/extras = "None" + if(CHECK_BITFIELD(genital.genital_flags, GENITAL_CAN_STUFF)) + extras = "Allows egg stuffing" + + genital_entry["extras"] = extras + genital_entry["visibility"] = visibility + genital_entry["possible_choices"] = GLOB.genitals_visibility_toggles + genital_entry["extra_choices"] = list(GEN_ALLOW_EGG_STUFFING) + genital_entry["can_arouse"] = ( + !!CHECK_BITFIELD(genital.genital_flags, GENITAL_CAN_AROUSE) \ + && !(HAS_TRAIT(genital_holder, TRAIT_PERMABONER) \ + || HAS_TRAIT(genital_holder, TRAIT_NEVERBONER))) + genital_entry["arousal_state"] = genital.aroused_state + if(istype(genital, /obj/item/organ/genital/penis)) + var/obj/item/organ/genital/penis/peepee = genital + genital_entry["max_size"] = peepee.max_length ? peepee.max_length : 0 + genital_entry["min_size"] = peepee.min_length ? peepee.min_length : 0 + else + genital_entry["max_size"] = genital.max_size ? genital.max_size : 0 + genital_entry["min_size"] = genital.min_size ? genital.min_size : 0 + + //fluids + if(CHECK_BITFIELD(genital.genital_flags, GENITAL_FUID_PRODUCTION) || CHECK_BITFIELD(genital?.linked_organ?.genital_flags, GENITAL_FUID_PRODUCTION)) + var/fluids = genital.get_fluid_fraction() + genital_entry["fluid"] = fluids + + //equipments + if(genital.is_exposed()) + var/list/equipments = list() + for(var/obj/equipment in genital.contents) + var/list/equipment_entry = list() + equipment_entry["name"] = equipment.name + equipment_entry["key"] = REF(equipment) + equipments += list(equipment_entry) + genital_entry["possible_equipment_choices"] = GLOB.genitals_interactions + genital_entry["equipments"] = equipments + + genitals += list(genital_entry) + + if(!genital_holder.getorganslot(ORGAN_SLOT_ANUS) && user_is_target) + var/simulated_ass = list() + simulated_ass["name"] = "Anus" + simulated_ass["key"] = "anus" + var/visibility = "Invalid" + switch(genital_holder.anus_exposed) + if(1) + visibility = "Always visible" + if(0) + visibility = "Hidden by underwear" + else + visibility = "Always hidden" + simulated_ass["visibility"] = visibility + simulated_ass["possible_choices"] = GLOB.genitals_visibility_toggles - GEN_VISIBLE_NO_CLOTHES + genitals += list(simulated_ass) + .["genitals"] = genitals + +/datum/genitals_menu/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state) + if(..()) + return + switch(action) + if("genital") + var/mob/living/carbon/self = usr + if(self != target) + return FALSE + if("visibility" in params) + if(params["genital"] == "anus") + self.anus_toggle_visibility(params["visibility"]) + return TRUE + var/obj/item/organ/genital/genital = locate(params["genital"], self.internal_organs) + if(genital && (genital in self.internal_organs)) + genital.toggle_visibility(params["visibility"]) + return TRUE + if("set_arousal" in params) + var/obj/item/organ/genital/genital = locate(params["genital"], self.internal_organs) + if(!genital || (genital \ + && (!CHECK_BITFIELD(genital.genital_flags, GENITAL_CAN_AROUSE) \ + || HAS_TRAIT(self, TRAIT_PERMABONER) \ + || HAS_TRAIT(self, TRAIT_NEVERBONER)))) + return FALSE + var/original_state = genital.aroused_state + genital.set_aroused_state(params["set_arousal"])// i'm not making it just `!aroused_state` because + if(original_state != genital.aroused_state) // someone just might port skyrat's new genitals + to_chat(self, "[genital.aroused_state ? genital.arousal_verb : genital.unarousal_verb].") + . = TRUE + else + to_chat(self, "You can't make that genital [genital.aroused_state ? "unaroused" : "aroused"]!") + . = FALSE + genital.update_appearance() + if(ishuman(self)) + var/mob/living/carbon/human/human = self + human.update_genitals() + return + if("max_size" in params) + var/obj/item/organ/genital/genital = locate(params["genital"], self.internal_organs) + if(!genital) + return FALSE + if(istype(genital, /obj/item/organ/genital/penis)) + var/obj/item/organ/genital/penis/peepee = genital + if(params["max_size"]) + var/new_max_size = clamp(params["max_size"], peepee.length, INFINITY) + peepee.max_length = new_max_size + else + peepee.max_length = 0 + else + if(params["max_size"]) + var/new_max_size = clamp(params["max_size"], genital.size, INFINITY) + genital.max_size = new_max_size + else + genital.max_size = 0 + if("min_size" in params) + var/obj/item/organ/genital/genital = locate(params["genital"], self.internal_organs) + if(!genital) + return FALSE + if(istype(genital, /obj/item/organ/genital/penis)) + var/obj/item/organ/genital/penis/peepee = genital + var/new_min_size = clamp(params["min_size"], 0, peepee.length) + peepee.min_length = new_min_size + else + var/new_min_size = clamp(params["min_size"], 0, genital.size) + genital.min_size = new_min_size + else + return FALSE + if("equipment") + var/mob/living/carbon/actual_target = target || usr + var/mob/living/carbon/self = usr + if(get_dist(actual_target, self) > 1) + to_chat(self, span_warning("You're too far away!")) + return FALSE + var/obj/item/organ/genital/genital = locate(params["genital"], actual_target.internal_organs) + if(!(genital && (genital in actual_target.internal_organs))) + return FALSE + switch(params["equipment_action"]) + if("remove") + var/obj/item/selected_item = locate(params["equipment"], genital.contents) + if(selected_item) + if(!do_mob(self, actual_target, 5 SECONDS)) + return FALSE + if(!self.put_in_hands(selected_item)) + self.transferItemToLoc(get_turf(self)) + return TRUE + return FALSE + if("insert") + var/obj/item/stuff = self.get_active_held_item() + if(!istype(stuff)) + to_chat(self, span_warning("You need to hold an item to insert it!")) + return FALSE + stuff.insert_item_organ(self, self, genital) diff --git a/modular_splurt/code/datums/traits/neutral.dm b/modular_splurt/code/datums/traits/neutral.dm index 44a529dd06..ac068b29ee 100644 --- a/modular_splurt/code/datums/traits/neutral.dm +++ b/modular_splurt/code/datums/traits/neutral.dm @@ -820,3 +820,29 @@ var/obj/item/clothing/mask/gas/cosmetic/gasmask = new(get_turf(quirk_holder)) // Uses a custom gas mask H.equip_to_slot(gasmask, ITEM_SLOT_MASK) H.regenerate_icons() + +/datum/quirk/body_morpher + name = "Body Morpher" + desc = "Somehow you developed an ability allowing your body to morph and shift itself to modify bodyparts, much like a slimeperson can." + value = 0 + mob_trait = TRAIT_BODY_MORPHER + gain_text = span_notice("Your body feels more malleable...") + lose_text = span_notice("Your body is more firm.") + medical_record_text = "Patient's body seems unusually malleable." + var/datum/action/innate/ability/humanoid_customization/alter_form_action + +/datum/quirk/body_morpher/add() + // Define quirk mob + var/mob/living/carbon/human/quirk_mob = quirk_holder + + // Add quirk ability action datum + alter_form_action = new + alter_form_action.Grant(quirk_mob) + +/datum/quirk/body_morpher/remove() + // Define quirk mob + var/mob/living/carbon/human/quirk_mob = quirk_holder + + // Remove quirk ability action datum + alter_form_action.Remove(quirk_mob) + QDEL_NULL(alter_form_action) diff --git a/modular_splurt/code/game/machinery/research_table.dm b/modular_splurt/code/game/machinery/research_table.dm index c984bec303..899910fac5 100644 --- a/modular_splurt/code/game/machinery/research_table.dm +++ b/modular_splurt/code/game/machinery/research_table.dm @@ -139,7 +139,7 @@ for(var/obj/item/organ/genital/genital in buckled_mob.internal_organs) if(istype(genital, /obj/item/organ/genital/breasts)) var/obj/item/organ/genital/breasts/breasts = genital - points_awarded += breasts.fluid_rate + breasts.breast_values[breasts.size] // Breasts use letters instead of numbers! + points_awarded += breasts.fluid_rate + GLOB.breast_values[breasts.size] continue points_awarded += genital.fluid_rate + genital.size points_awarded *= tier diff --git a/modular_splurt/code/game/objects/items.dm b/modular_splurt/code/game/objects/items.dm index c320da9586..a39406bb68 100644 --- a/modular_splurt/code/game/objects/items.dm +++ b/modular_splurt/code/game/objects/items.dm @@ -55,10 +55,17 @@ return if(locate(src.type) in target_organ.contents) - to_chat(user, span_notice("\The [target]'s [target_organ] already has a rod inside!")) + if(user == target) + to_chat(user, span_notice("You already have a rod inside your [target_organ]!")) + else + to_chat(user, span_notice("\The [target]'s [target_organ] already has a rod inside!")) return - target.visible_message(span_warning("\The [user] is trying to insert a rod inside \the [target]!"),\ + if(user == target) + target.visible_message(span_warning("\The [user] is trying to insert a rod inside themselves!"),\ + span_warning("You try to insert a rod inside yourself!")) + else + target.visible_message(span_warning("\The [user] is trying to insert a rod inside \the [target]!"),\ span_warning("\The [user] is trying to insert a rod inside you!")) if(!do_mob(user, target, 4 SECONDS)) @@ -81,15 +88,18 @@ return if(locate(src.type) in target_organ.contents) - to_chat(user, span_notice("\The [target]'s [target_organ] already has a vibrator inside!")) + if(user == target) + to_chat(user, span_notice("You already have a vibrator inside your [target_organ]!")) + else + to_chat(user, span_notice("\The [target]'s [target_organ] already has a vibrator inside!")) return - if(style == "long") - target.visible_message(span_warning("\The [user] is trying to insert a vibrator inside \the [target]!"),\ - span_warning("\The [user] is trying to insert a vibrator inside you!")) + if(user == target) + target.visible_message(span_warning("\The [user] is trying to [style == "long" ? "insert" : "attach"] a vibrator [style == "long" ? "inside" : "to"] themselves!"),\ + span_warning("You try to [style == "long" ? "insert" : "attach"] a vibrator [style == "long" ? "inside" : "to"] yourself!")) else - target.visible_message(span_userlove("\The [user] is trying to attach a vibrator to \the [target]!"),\ - span_warning("\The [user] is trying to attach a vibrator to you!")) + target.visible_message(span_warning("\The [user] is trying to [style == "long" ? "insert" : "attach"] a vibrator [style == "long" ? "inside" : "to"] \the [target]!"),\ + span_warning("\The [user] is trying to [style == "long" ? "insert" : "attach"] a vibrator [style == "long" ? "inside" : "to"] you!")) if(!do_mob(user, target, 5 SECONDS)) return @@ -115,7 +125,11 @@ to_chat(user, span_warning("This genital can't be stuffed!")) return - target.visible_message(span_warning("\The [user] is trying to insert an egg inside \the [target]!"),\ + if(user == target) + target.visible_message(span_warning("\The [user] is trying to insert an egg inside themselves!"),\ + span_warning("You try to insert an egg inside yourself!")) + else + target.visible_message(span_warning("\The [user] is trying to insert an egg inside \the [target]!"),\ span_warning("\The [user] is trying to insert an egg inside you!")) if(!do_mob(user, target, 5 SECONDS)) diff --git a/modular_splurt/code/modules/antagonists/qareen/qareen_abilities.dm b/modular_splurt/code/modules/antagonists/qareen/qareen_abilities.dm index 8963ea6379..4af34c922c 100644 --- a/modular_splurt/code/modules/antagonists/qareen/qareen_abilities.dm +++ b/modular_splurt/code/modules/antagonists/qareen/qareen_abilities.dm @@ -41,7 +41,7 @@ if(do_after(src, rand(10, 20), 0, target)) //did they get deleted in that second? // var/main_fluid = lowertext(fluid_source.get_master_reagent_name()) // doesn't work no more (should delete probably) var/main_fluid = G.get_fluid_name() - var/fluid_ammount = clamp((G.fluid_rate * ((world.time - G.last_orgasmed) / (10 SECONDS)) * G.fluid_mult),0,G.fluid_max_volume) + var/fluid_ammount = G.get_fluid() if (fluid_ammount <= 2) to_chat(src, span_revennotice("[target.p_their(TRUE)] [G.name] spasms pitifully, almost nothing will come out.")) else diff --git a/modular_splurt/code/modules/arousal/genitals.dm b/modular_splurt/code/modules/arousal/genitals.dm index b52a1aab76..6c935614fd 100644 --- a/modular_splurt/code/modules/arousal/genitals.dm +++ b/modular_splurt/code/modules/arousal/genitals.dm @@ -1,4 +1,6 @@ /obj/item/organ/genital + var/max_size = 6 + var/min_size = 1 var/datum/reagents/climax_fluids var/datum/reagent/original_fluid_id var/datum/reagent/default_fluid_id @@ -10,6 +12,15 @@ fluid_max_volume += (modifier*2.5)*(get_size(owner)-1) fluid_rate += (modifier/10)*(get_size(owner)-1) +/obj/item/organ/genital/proc/size_to_state() + return size + +/obj/item/organ/genital/proc/get_fluid() + return clamp(fluid_rate * ((world.time - last_orgasmed) / (10 SECONDS)) * fluid_mult, 0, fluid_max_volume) + +/obj/item/organ/genital/proc/get_fluid_fraction() + return get_fluid() / fluid_max_volume + /obj/item/organ/genital/proc/climax_modify_size(mob/living/partner, obj/item/organ/genital/source_gen) return diff --git a/modular_splurt/code/modules/arousal/organs/belly.dm b/modular_splurt/code/modules/arousal/organs/belly.dm index 44ae1ffafa..8f17da4a32 100644 --- a/modular_splurt/code/modules/arousal/organs/belly.dm +++ b/modular_splurt/code/modules/arousal/organs/belly.dm @@ -25,8 +25,8 @@ if(!owner) return -/obj/item/organ/genital/belly/modify_size(modifier, min = -INFINITY, max = BELLY_SIZE_MAX) - var/new_value = clamp(size_cached + modifier, min, max) +/obj/item/organ/genital/belly/modify_size(modifier, min = -INFINITY, max = INFINITY) + var/new_value = clamp(size_cached + modifier, max(min, min_size ? min_size : -INFINITY), min(max_size ? max_size : INFINITY, max)) if(new_value == size_cached) return prev_size = size_cached @@ -95,6 +95,8 @@ else color = "#[D.features["belly_color"]]" size = D.features["belly_size"] + max_size = D.features["belly_max_size"] + min_size = D.features["belly_min_size"] prev_size = size size_cached = size original_fluid_id = fluid_id diff --git a/modular_splurt/code/modules/arousal/organs/breasts.dm b/modular_splurt/code/modules/arousal/organs/breasts.dm index c815295e28..a4f3688d00 100644 --- a/modular_splurt/code/modules/arousal/organs/breasts.dm +++ b/modular_splurt/code/modules/arousal/organs/breasts.dm @@ -13,8 +13,8 @@ fluid_id = initial(fluid_id) original_fluid_id = fluid_id . = ..() - fluid_max_volume += ((cached_size - breast_values[initial(size)])*2.5)*(owner ? get_size(owner) : 1) - fluid_rate += ((cached_size - breast_values[initial(size)])/10)*(owner ? get_size(owner) : 1) + fluid_max_volume += ((size - initial(size))*2.5)*(owner ? get_size(owner) : 1) + fluid_rate += ((size - initial(size))/10)*(owner ? get_size(owner) : 1) /obj/item/organ/genital/breasts/climax_modify_size(mob/living/partner, obj/item/organ/genital/source_gen) if(!(owner.client?.prefs.cit_toggles & BREAST_ENLARGEMENT)) diff --git a/modular_splurt/code/modules/arousal/organs/testicles.dm b/modular_splurt/code/modules/arousal/organs/testicles.dm index 16a5f2b152..a8180ee4f4 100644 --- a/modular_splurt/code/modules/arousal/organs/testicles.dm +++ b/modular_splurt/code/modules/arousal/organs/testicles.dm @@ -1,6 +1,18 @@ /obj/item/organ/genital/testicles default_fluid_id = /datum/reagent/consumable/semen +/obj/item/organ/genital/testicles/get_fluid() + if(linked_organ) + return clamp(linked_organ.fluid_rate * ((world.time - linked_organ.last_orgasmed) / (10 SECONDS)) * linked_organ.fluid_mult, 0, linked_organ.fluid_max_volume) + else + return 0 + +/obj/item/organ/genital/testicles/get_fluid_fraction() + if(linked_organ) + return get_fluid() / linked_organ.fluid_max_volume + else + return 0 + /obj/item/organ/genital/testicles/get_features(mob/living/carbon/human/H) var/datum/dna/D = H.dna if(D.features["balls_fluid"]) diff --git a/modular_splurt/code/modules/arousal/organs/womb.dm b/modular_splurt/code/modules/arousal/organs/womb.dm index 7918365ea9..3a8bc4adf4 100644 --- a/modular_splurt/code/modules/arousal/organs/womb.dm +++ b/modular_splurt/code/modules/arousal/organs/womb.dm @@ -1,6 +1,18 @@ /obj/item/organ/genital/womb default_fluid_id = /datum/reagent/consumable/semen/femcum +/obj/item/organ/genital/womb/get_fluid() + if(linked_organ) + return clamp(linked_organ.fluid_rate * ((world.time - linked_organ.last_orgasmed) / (10 SECONDS)) * linked_organ.fluid_mult, 0, linked_organ.fluid_max_volume) + else + return 0 + +/obj/item/organ/genital/womb/get_fluid_fraction() + if(linked_organ) + return get_fluid() / linked_organ.fluid_max_volume + else + return 0 + /obj/item/organ/genital/womb/get_features(mob/living/carbon/human/H) var/datum/dna/D = H.dna if(D.features["womb_fluid"]) diff --git a/modular_splurt/code/modules/client/preferences.dm b/modular_splurt/code/modules/client/preferences.dm index 58b8789f76..3ceec318df 100644 --- a/modular_splurt/code/modules/client/preferences.dm +++ b/modular_splurt/code/modules/client/preferences.dm @@ -1195,7 +1195,7 @@ var/new_marking_color = input(user, "Choose your character's marking color:", "Character Preference","#"+color_list[color_number]) as color|null if(new_marking_color) var/temp_hsv = RGBtoHSV(new_marking_color) - if((MUTCOLORS_PARTSONLY in pref_species.species_traits) || ReadHSV(temp_hsv)[3] >= ReadHSV(MINIMUM_MUTANT_COLOR)[3]) // mutantcolors must be bright, but only if they affect the skin + if((MUTCOLORS_PARTSONLY in pref_species.species_traits) || ReadHSV(temp_hsv)[3] >= ReadHSV(MINIMUM_MUTANT_COLOR)[3] || !CONFIG_GET(flag/character_color_limits)) // mutantcolors must be bright, but only if they affect the skin color_list[color_number] = "#[sanitize_hexcolor(new_marking_color, 6)]" else to_chat(user, "Invalid color. Your color is not bright enough.") diff --git a/modular_splurt/code/modules/mob/living/emotes.dm b/modular_splurt/code/modules/mob/living/emotes.dm index ec5f1a1d88..0911f4b791 100644 --- a/modular_splurt/code/modules/mob/living/emotes.dm +++ b/modular_splurt/code/modules/mob/living/emotes.dm @@ -639,6 +639,36 @@ emote_sound = 'modular_splurt/sound/voice/agony.ogg' emote_cooldown = 7 SECONDS +/datum/emote/living/audio/wtune + key = "whistle_tune" + key_third_person = "whistle_tunes" + message = "whistles a tune." + message_mime = "makes an expression as if whistling." + emote_sound = 'modular_splurt/sound/voice/wtune1.ogg' + emote_cooldown = 4.55 SECONDS // Uses longest sound's time. + +/datum/emote/living/audio/wtune/run_emote(mob/user, params) + // Set random emote sound + emote_sound = pick('modular_splurt/sound/voice/wtune1.ogg', 'modular_splurt/sound/voice/wtune2.ogg') + + // Return normally + . = ..() + +/datum/emote/living/audio/terror + key = "terror" + key_third_person = "terrors" + message = "whistles some dreadful tune..." + message_mime = "stares with aura full of dread..." + emote_sound = 'modular_splurt/sound/voice/terror1.ogg' + emote_cooldown = 13.07 SECONDS // Uses longest sound's time. + +/datum/emote/living/audio/terror/run_emote(mob/user, params) + // Set random emote sound + emote_sound = pick('modular_splurt/sound/voice/terror1.ogg', 'modular_splurt/sound/voice/terror2.ogg') + + // Return normally + . = ..() + /datum/emote/living/audio/sicko key = "sicko" key_third_person = "sickos" diff --git a/modular_splurt/code/modules/reagents/reagent_containers/spray.dm b/modular_splurt/code/modules/reagents/reagent_containers/spray.dm index 01d0dbf5fc..5b3c46d106 100644 --- a/modular_splurt/code/modules/reagents/reagent_containers/spray.dm +++ b/modular_splurt/code/modules/reagents/reagent_containers/spray.dm @@ -23,3 +23,17 @@ // Return normally . = ..() + +/obj/item/reagent_containers/spray/ultimate + name = "ultimate spray bottle" + desc = "A bottle to spray above them all." + volume = 750 + amount_per_transfer_from_this = 2 + current_range = 5 + spray_range = 5 + stream_range = 5 + stream_amount = 2 + icon = 'modular_splurt/icons/obj/janitor.dmi' + icon_state = "cleaneralien" + item_state = "cleaneralien" + custom_materials = list(/datum/material/iron = 2250, /datum/material/glass = 2250, /datum/material/plasma = 2250, /datum/material/diamond = 370, /datum/material/bluespace = 370) diff --git a/modular_splurt/code/modules/research/designs/misc_designs.dm b/modular_splurt/code/modules/research/designs/misc_designs.dm index e6105fd3f3..3572f55c45 100644 --- a/modular_splurt/code/modules/research/designs/misc_designs.dm +++ b/modular_splurt/code/modules/research/designs/misc_designs.dm @@ -1,2 +1,19 @@ /datum/design/light_replacer_blue/New() departmental_flags |= DEPARTMENTAL_FLAG_SCIENCE + . = ..() + +/datum/design/bluespacespray/New() + departmental_flags |= DEPARTMENTAL_FLAG_SERVICE + . = ..() + + +/datum/design/ultimatespray + name = "Ultimate Spray" + desc = "The ultimate sprayer, no mess shall go unclean today." + id = "ultimatespray" + build_type = PROTOLATHE + materials = list(/datum/material/iron = 2250, /datum/material/glass = 2250, /datum/material/plasma = 2250, /datum/material/diamond = 370, /datum/material/bluespace = 370) + build_path = /obj/item/reagent_containers/spray/ultimate + reagents_list = list(/datum/reagent/liquid_dark_matter = 10) + category = list("Misc", "Medical Designs") + departmental_flags = DEPARTMENTAL_FLAG_MEDICAL | DEPARTMENTAL_FLAG_SCIENCE | DEPARTMENTAL_FLAG_SERVICE diff --git a/modular_splurt/code/modules/research/techweb/nodes/janitor_nodes.dm b/modular_splurt/code/modules/research/techweb/nodes/janitor_nodes.dm new file mode 100644 index 0000000000..08e2cecee5 --- /dev/null +++ b/modular_splurt/code/modules/research/techweb/nodes/janitor_nodes.dm @@ -0,0 +1,7 @@ +/datum/techweb_node/ultispray + id = "ultispray" + display_name = "Alien Janitoring" + description = "Clean messes like Abductors clean up witnesses" + prereq_ids = list("alien_stockparts", "bspspray") + design_ids = list("ultimatespray") + research_costs = list(TECHWEB_POINT_TYPE_GENERIC = 1500) diff --git a/modular_splurt/code/modules/vending/barkbox.dm b/modular_splurt/code/modules/vending/barkbox.dm index 6c4cebb7c8..a79eb70aed 100644 --- a/modular_splurt/code/modules/vending/barkbox.dm +++ b/modular_splurt/code/modules/vending/barkbox.dm @@ -3,7 +3,8 @@ /obj/item/clothing/neck/petcollar/spike = 5, /obj/item/clothing/neck/petcollar/holo = 5, /obj/item/clothing/neck/petcollar/casino = 5, - /obj/item/clothing/neck/petcollar/handmade = 5 + /obj/item/clothing/neck/petcollar/handmade = 5, + /obj/item/leash = 4 ) var/list/extra_contraband = list( /obj/item/clothing/neck/petcollar/locked/spike = 2, diff --git a/modular_splurt/icons/obj/janitor.dmi b/modular_splurt/icons/obj/janitor.dmi new file mode 100644 index 0000000000..ae176c890d Binary files /dev/null and b/modular_splurt/icons/obj/janitor.dmi differ diff --git a/modular_splurt/sound/voice/terror1.ogg b/modular_splurt/sound/voice/terror1.ogg new file mode 100644 index 0000000000..0ad7f45b5b Binary files /dev/null and b/modular_splurt/sound/voice/terror1.ogg differ diff --git a/modular_splurt/sound/voice/terror2.ogg b/modular_splurt/sound/voice/terror2.ogg new file mode 100644 index 0000000000..b56724d42f Binary files /dev/null and b/modular_splurt/sound/voice/terror2.ogg differ diff --git a/modular_splurt/sound/voice/wtune1.ogg b/modular_splurt/sound/voice/wtune1.ogg new file mode 100644 index 0000000000..ba670f1d03 Binary files /dev/null and b/modular_splurt/sound/voice/wtune1.ogg differ diff --git a/modular_splurt/sound/voice/wtune2.ogg b/modular_splurt/sound/voice/wtune2.ogg new file mode 100644 index 0000000000..9d265fa974 Binary files /dev/null and b/modular_splurt/sound/voice/wtune2.ogg differ diff --git a/tgstation.dme b/tgstation.dme index dc3e570b87..be81168a99 100644 --- a/tgstation.dme +++ b/tgstation.dme @@ -3493,6 +3493,7 @@ #include "code\modules\research\techweb\nodes\computer_hud_nodes.dm" #include "code\modules\research\techweb\nodes\engineering_nodes.dm" #include "code\modules\research\techweb\nodes\export_nodes.dm" +#include "modular_splurt\code\modules\research\techweb\nodes\janitor_nodes.dm" #include "code\modules\research\techweb\nodes\mecha_nodes.dm" #include "code\modules\research\techweb\nodes\medical_nodes.dm" #include "code\modules\research\techweb\nodes\misc_nodes.dm" @@ -4350,6 +4351,7 @@ #include "modular_splurt\code\datums\elements\smalltalk.dm" #include "modular_splurt\code\datums\elements\spooky.dm" #include "modular_splurt\code\datums\elements\wuv.dm" +#include "modular_splurt\code\datums\genitals\genitals_interface.dm" #include "modular_splurt\code\datums\interactions\cuddling.dm" #include "modular_splurt\code\datums\interactions\rope.dm" #include "modular_splurt\code\datums\interactions\lewd\_lewd.dm" diff --git a/tgui/packages/tgui/index.js b/tgui/packages/tgui/index.js index 31251cc35b..bd6f3f5302 100644 --- a/tgui/packages/tgui/index.js +++ b/tgui/packages/tgui/index.js @@ -7,6 +7,7 @@ // Themes import './styles/main.scss'; import './styles/themes/abductor.scss'; +import './styles/themes/hotpink.scss'; import './styles/themes/cardtable.scss'; import './styles/themes/hackerman.scss'; import './styles/themes/malfunction.scss'; diff --git a/tgui/packages/tgui/interfaces/GenitalConfig.tsx b/tgui/packages/tgui/interfaces/GenitalConfig.tsx new file mode 100644 index 0000000000..e1b595bfce --- /dev/null +++ b/tgui/packages/tgui/interfaces/GenitalConfig.tsx @@ -0,0 +1,313 @@ + +import { filter } from 'common/collections'; +import { flow } from 'common/fp'; +import { createSearch } from 'common/string'; +import { useBackend, useLocalState } from '../backend'; +import { BlockQuote, Button, LabeledList, Icon, NumberInput, Input, Section, Table, Tabs, Stack, ProgressBar, Divider } from '../components'; +import { Window } from '../layouts'; + + +type GenitalInfo = { + istargetself: boolean, + target_name: String, + genitals: GenitalData[]; +} + +type GenitalData = { + img: string, + name: string, + key: string, + description: string, + visibility: string, + extras: string, + extra_choices: string[], + possible_choices: string[], + can_arouse: boolean, + arousal_state: boolean, + fluid: number, + possible_equipment_choices: string[], + min_size: number, + max_size: number, + equipments: Equipment[], +} + +type Equipment = { + name: string, + key: string, +} + +export const GenitalConfig = (props, context) => { + const { act, data } = useBackend(context); + const genitals = data.genitals || []; + const [tabIndex, setTabIndex] = useLocalState(context, 'tabIndex', 0); + return ( + + + {data.target_name ? ( +
+ Interacting with {data.target_name} +
+ ) : null} + {genitals.length ? ( + <> +
+ + + + {genitals.map((genital, index) => ( + setTabIndex(index)} + > + {genital.name} + + ))} + + + + {genitals[tabIndex].description} + + +
+
+ + + + +
+ Fluid level: + +
+
+ {data.istargetself + ? : null} +
+
+
+
+
act('equipment', { + genital: genitals[tabIndex].key, + equipment_action: "insert", + })} + /> + )} + > + +
+ + ) : ( +
+ {data.target_name ? "They" : "You"} don't seem to have any genitals... + Or any that you could interact with. +
+ )} +
+
+ ); +}; + +const SelfConfig = (props, context) => { + const [tabIndex] = useLocalState(context, 'tabIndex', 0); + const { act, data } = useBackend(context); + const genital = data.genitals[tabIndex]; + return ( + + + {genital.img ? ( + + + + ) : null} + {typeof genital.max_size === "number" ? ( + + + + ) : null} + + + + + + ); +}; + +const SizeButtons = (props, context) => { + const [tabIndex] = useLocalState(context, 'tabIndex', 0); + const { act, data } = useBackend(context); + const genital = data.genitals[tabIndex]; + return ( +
+ + + + Max growth: + act('genital', { + genital: genital.key, + max_size: value, + } + )} + /> + + + + + Min shrink: + act('genital', { + genital: genital.key, + min_size: value, + } + )} + /> + + + +
+ ); +}; + +const ModeToIcon = { + "Always visible": "eye", + "Hidden by clothes": "tshirt", + "Hidden by underwear": "low-vision", + "Always hidden": "eye-slash", + "Allows egg stuffing": "egg", +}; + +const ToggleSettings = (props, context) => { + const [tabIndex] = useLocalState(context, 'tabIndex', 0); + const { act, data } = useBackend(context); + const genital = data.genitals[tabIndex]; + return ( + + {genital.possible_choices.map(choice => ( + +