diff --git a/code/datums/components/mind_linker.dm b/code/datums/components/mind_linker.dm
index 2a6c20b2160..2b8d3899173 100644
--- a/code/datums/components/mind_linker.dm
+++ b/code/datums/components/mind_linker.dm
@@ -10,10 +10,6 @@
var/network_name = "Mind Link"
/// The color of the network when talking in chat
var/chat_color
- /// The message sent to someone when linked up.
- var/link_message
- /// The message sent to someone when unlinked.
- var/unlink_message
/// A list of all signals that will call qdel() on our component if triggered. Optional.
var/list/signals_which_destroy_us
/// A callback invoked after an unlink is done. Optional.
@@ -24,25 +20,25 @@
var/speech_action_icon_state = "link_speech"
/// The icon background for the speech action handed out.
var/speech_action_background_icon_state = "bg_alien"
- /// The master's linking action, which allows them to link people to the network.
- var/datum/action/linker_action
+ /// The border icon state for the speech action handed out.
+ var/speech_action_overlay_state = "bg_alien_border"
/// The master's speech action. The owner of the link shouldn't lose this as long as the link remains.
- var/datum/action/innate/linked_speech/master_speech
+ VAR_FINAL/datum/action/innate/linked_speech/master_speech
/// An assoc list of [mob/living]s to [datum/action/innate/linked_speech]s. All the mobs that are linked to our network.
- var/list/mob/living/linked_mobs = list()
+ VAR_FINAL/list/mob/living/linked_mobs = list()
/datum/component/mind_linker/Initialize(
+ // Customization related settings
network_name = "Mind Link",
chat_color = "#008CA2",
- linker_action_path,
- link_message,
- unlink_message,
- signals_which_destroy_us,
- datum/callback/post_unlink_callback,
speech_action_icon = 'icons/mob/actions/actions_slime.dmi',
speech_action_icon_state = "link_speech",
speech_action_background_icon_state = "bg_alien",
- )
+ speech_action_overlay_state = "bg_alien_border",
+ // Optional
+ signals_which_destroy_us,
+ datum/callback/post_unlink_callback,
+)
if(!isliving(parent))
return COMPONENT_INCOMPATIBLE
@@ -51,34 +47,22 @@
src.network_name = network_name
src.chat_color = chat_color
- src.link_message = link_message || "You are now connected to [owner.real_name]'s [network_name]."
- src.unlink_message = unlink_message || "You are no longer connected to [owner.real_name]'s [network_name]."
+ src.speech_action_icon = speech_action_icon
+ src.speech_action_icon_state = speech_action_icon_state
+ src.speech_action_background_icon_state = speech_action_background_icon_state
if(islist(signals_which_destroy_us))
src.signals_which_destroy_us = signals_which_destroy_us
if(post_unlink_callback)
src.post_unlink_callback = post_unlink_callback
- src.speech_action_icon = speech_action_icon
- src.speech_action_icon_state = speech_action_icon_state
- src.speech_action_background_icon_state = speech_action_background_icon_state
-
- if(ispath(linker_action_path))
- linker_action = new linker_action_path(src)
- linker_action.Grant(owner)
- else
- stack_trace("[type] was created without a valid linker_action_path. No one will be able to link to it.")
-
master_speech = new(src)
master_speech.Grant(owner)
- to_chat(owner, span_boldnotice("You establish a [network_name], allowing you to link minds to communicate telepathically."))
-
/datum/component/mind_linker/Destroy(force, silent)
for(var/mob/living/remaining_mob as anything in linked_mobs)
unlink_mob(remaining_mob)
linked_mobs.Cut()
- QDEL_NULL(linker_action)
QDEL_NULL(master_speech)
QDEL_NULL(post_unlink_callback)
return ..()
@@ -99,10 +83,6 @@
/datum/component/mind_linker/proc/link_mob(mob/living/to_link)
if(QDELETED(to_link) || to_link.stat == DEAD)
return FALSE
- if(HAS_TRAIT(to_link, TRAIT_MINDSHIELD)) // Mindshield implant - no dice
- return FALSE
- if(to_link.can_block_magic(MAGIC_RESISTANCE_MIND, charge_cost = 0))
- return FALSE
if(linked_mobs[to_link])
return FALSE
@@ -110,20 +90,19 @@
if(to_link == owner)
return FALSE
- to_chat(to_link, span_notice(link_message))
- to_chat(owner, span_notice("You connect [to_link]'s mind to your [network_name]."))
-
- for(var/mob/living/other_link as anything in linked_mobs)
- to_chat(other_link, span_notice("You feel a new presence within [owner.real_name]'s [network_name]."))
-
var/datum/action/innate/linked_speech/new_link = new(src)
new_link.Grant(to_link)
linked_mobs[to_link] = new_link
- RegisterSignals(to_link, list(COMSIG_LIVING_DEATH, COMSIG_PARENT_QDELETING, COMSIG_MINDSHIELD_IMPLANTED), PROC_REF(unlink_mob))
+ RegisterSignals(to_link, list(COMSIG_LIVING_DEATH, COMSIG_PARENT_QDELETING), PROC_REF(sig_unlink_mob))
return TRUE
+/datum/component/mind_linker/proc/sig_unlink_mob(mob/living/to_unlink)
+ SIGNAL_HANDLER
+
+ unlink_mob(to_unlink)
+
/**
* Unlinks [to_unlink] from our network, deleting their speech action
* and cleaning up anything involved.
@@ -131,25 +110,17 @@
* Also invokes post_unlink_callback, if supplied.
*/
/datum/component/mind_linker/proc/unlink_mob(mob/living/to_unlink)
- SIGNAL_HANDLER
-
if(!linked_mobs[to_unlink])
- return
+ return FALSE
- to_chat(to_unlink, span_warning(unlink_message))
post_unlink_callback?.Invoke(to_unlink)
- UnregisterSignal(to_unlink, list(COMSIG_LIVING_DEATH, COMSIG_PARENT_QDELETING, COMSIG_MINDSHIELD_IMPLANTED))
+ UnregisterSignal(to_unlink, list(COMSIG_LIVING_DEATH, COMSIG_PARENT_QDELETING))
var/datum/action/innate/linked_speech/old_link = linked_mobs[to_unlink]
linked_mobs -= to_unlink
qdel(old_link)
-
- var/mob/living/owner = parent
-
- to_chat(owner, span_warning("You feel someone disconnect from your [network_name]."))
- for(var/mob/living/other_link as anything in linked_mobs)
- to_chat(other_link, span_warning("You feel a pressence disappear from [owner.real_name]'s [network_name]."))
+ return TRUE
/**
* Signal proc sent from any signals given to us initialize.
@@ -164,6 +135,84 @@
qdel(src)
+/// Subtype of mind linker (I know) which is more active rather than passive,
+/// which involves the master linking people manually rather than people being added automatically.
+/datum/component/mind_linker/active_linking
+ /// The message sent to someone when linked up.
+ var/link_message
+ /// The message sent to someone when unlinked.
+ var/unlink_message
+ /// The master's linking action, which allows them to link people to the network.
+ VAR_FINAL/datum/action/linker_action
+
+/datum/component/mind_linker/active_linking/Initialize(
+ // Customization related settings
+ network_name = "Mind Link",
+ chat_color = "#008CA2",
+ speech_action_icon = 'icons/mob/actions/actions_slime.dmi',
+ speech_action_icon_state = "link_speech",
+ speech_action_background_icon_state = "bg_alien",
+ speech_action_overlay_state = "bg_alien_border",
+ // Optional
+ signals_which_destroy_us,
+ datum/callback/post_unlink_callback,
+ // Optional for this subtype
+ link_message,
+ unlink_message,
+ // Required for this subtype
+ linker_action_path,
+)
+
+ . = ..()
+ if(. == COMPONENT_INCOMPATIBLE)
+ return
+
+ var/mob/living/owner = parent
+ src.link_message = link_message || "You are now connected to [owner.real_name]'s [network_name]."
+ src.unlink_message = unlink_message || "You are no longer connected to [owner.real_name]'s [network_name]."
+
+ if(ispath(linker_action_path))
+ linker_action = new linker_action_path(src)
+ linker_action.Grant(owner)
+ else
+ stack_trace("[type] was created without a valid linker_action_path. No one will be able to link to it.")
+
+ to_chat(owner, span_boldnotice("You establish a [network_name], allowing you to link minds to communicate telepathically."))
+
+/datum/component/mind_linker/active_linking/Destroy()
+ QDEL_NULL(linker_action)
+ return ..()
+
+/datum/component/mind_linker/active_linking/link_mob(mob/living/to_link)
+ if(HAS_TRAIT(to_link, TRAIT_MINDSHIELD)) // Mindshield implant - no dice
+ return FALSE
+ if(to_link.can_block_magic(MAGIC_RESISTANCE_MIND, charge_cost = 0))
+ return FALSE
+
+ . = ..()
+ if(!.)
+ return
+
+ RegisterSignal(to_link, COMSIG_MINDSHIELD_IMPLANTED, PROC_REF(sig_unlink_mob))
+ var/mob/living/owner = parent
+ to_chat(to_link, span_notice(link_message))
+ to_chat(owner, span_notice("You connect [to_link]'s mind to your [network_name]."))
+ for(var/mob/living/other_link as anything in linked_mobs)
+ to_chat(other_link, span_notice("You feel a new presence within [owner.real_name]'s [network_name]."))
+
+/datum/component/mind_linker/active_linking/unlink_mob(mob/living/to_unlink)
+ . = ..()
+ if(!.)
+ return
+
+ UnregisterSignal(to_unlink, COMSIG_MINDSHIELD_IMPLANTED)
+ var/mob/living/owner = parent
+ to_chat(to_unlink, span_warning(unlink_message))
+ to_chat(owner, span_warning("You feel someone disconnect from your [network_name]."))
+ for(var/mob/living/other_link as anything in linked_mobs)
+ to_chat(other_link, span_warning("You feel a pressence disappear from [owner.real_name]'s [network_name]."))
+
+// Used in mind linker to talk to everyone in the network.
/datum/action/innate/linked_speech
name = "Mind Link Speech"
desc = "Send a psychic message to everyone connected to your Link."
diff --git a/code/datums/proximity_monitor/fields/gravity.dm b/code/datums/proximity_monitor/fields/gravity.dm
index 181e74157cd..0f6e8ba9d55 100644
--- a/code/datums/proximity_monitor/fields/gravity.dm
+++ b/code/datums/proximity_monitor/fields/gravity.dm
@@ -1,3 +1,4 @@
+// Proximity monitor applies forced gravity to all turfs in range.
/datum/proximity_monitor/advanced/gravity
edge_is_a_field = TRUE
var/gravity_value = 0
@@ -10,9 +11,10 @@
/datum/proximity_monitor/advanced/gravity/setup_field_turf(turf/target)
. = ..()
- if (isnull(modified_turfs[target]))
+ if(!isnull(modified_turfs[target]))
+ return
+ if(HAS_TRAIT(target, TRAIT_FORCED_GRAVITY))
return
-
target.AddElement(/datum/element/forced_gravity, gravity_value)
modified_turfs[target] = gravity_value
@@ -20,5 +22,44 @@
. = ..()
if(isnull(modified_turfs[target]))
return
- target.RemoveElement(/datum/element/forced_gravity, modified_turfs[target])
+ var/grav_value = modified_turfs[target] || 0
+ target.RemoveElement(/datum/element/forced_gravity, grav_value)
modified_turfs -= target
+
+// Subtype which pops up a balloon alert when a mob enters the field
+/datum/proximity_monitor/advanced/gravity/warns_on_entrance
+ /// This is a list of mob refs that have recently entered the field.
+ /// We track it so that we don't spam a player who is stutter stepping in and out with balloon alerts.
+ var/list/recently_warned
+
+/datum/proximity_monitor/advanced/gravity/warns_on_entrance/setup_field_turf(turf/target)
+ . = ..()
+ for(var/mob/living/guy in target)
+ warn_mob(guy, target)
+
+/datum/proximity_monitor/advanced/gravity/warns_on_entrance/cleanup_field_turf(turf/target)
+ . = ..()
+ for(var/mob/living/guy in target)
+ warn_mob(guy, target)
+
+/datum/proximity_monitor/advanced/gravity/warns_on_entrance/field_edge_crossed(atom/movable/movable, turf/location)
+ . = ..()
+ if(isliving(movable))
+ warn_mob(movable, location)
+
+/datum/proximity_monitor/advanced/gravity/warns_on_entrance/field_edge_uncrossed(atom/movable/movable, turf/location)
+ . = ..()
+ if(isliving(movable))
+ warn_mob(movable, location)
+
+/datum/proximity_monitor/advanced/gravity/warns_on_entrance/proc/warn_mob(mob/living/to_warn, turf/location)
+ var/mob_ref_key = REF(to_warn)
+ if(mob_ref_key in recently_warned)
+ return
+
+ location.balloon_alert(to_warn, "gravity [(location in modified_turfs) ? "shifts!" : "reverts..."]")
+ LAZYADD(recently_warned, mob_ref_key)
+ addtimer(CALLBACK(src, PROC_REF(clear_recent_warning), mob_ref_key), 3 SECONDS)
+
+/datum/proximity_monitor/advanced/gravity/warns_on_entrance/proc/clear_recent_warning(mob_ref_key)
+ LAZYREMOVE(recently_warned, mob_ref_key)
diff --git a/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm b/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm
index 4321b6f83dd..bf500dc75f2 100644
--- a/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm
+++ b/code/game/gamemodes/dynamic/dynamic_rulesets_midround.dm
@@ -561,8 +561,6 @@
var/mob/living/simple_animal/hostile/space_dragon/S = new (pick(spawn_locs))
player_mind.transfer_to(S)
- player_mind.set_assigned_role(SSjob.GetJobType(/datum/job/space_dragon))
- player_mind.special_role = ROLE_SPACE_DRAGON
player_mind.add_antag_datum(/datum/antagonist/space_dragon)
playsound(S, 'sound/magic/ethereal_exit.ogg', 50, TRUE, -1)
diff --git a/code/modules/antagonists/space_dragon/carp_rift.dm b/code/modules/antagonists/space_dragon/carp_rift.dm
index 4d86dab3343..e87ceb3f071 100644
--- a/code/modules/antagonists/space_dragon/carp_rift.dm
+++ b/code/modules/antagonists/space_dragon/carp_rift.dm
@@ -18,13 +18,13 @@
if(!dragon)
return
var/area/rift_location = get_area(owner)
- if(!(rift_location.area_flags & VALID_TERRITORY))
- to_chat(owner, span_warning("You can't summon a rift here! Try summoning somewhere secure within the station!"))
+ if(!(rift_location in dragon.chosen_rift_areas))
+ owner.balloon_alert(owner, "can't summon a rift here!")
return
for(var/obj/structure/carp_rift/rift as anything in dragon.rift_list)
var/area/used_location = get_area(rift)
if(used_location == rift_location)
- to_chat(owner, span_warning("You've already summoned a rift in this area! You have to summon again somewhere else!"))
+ owner.balloon_alert(owner, "already summoned a rift here!")
return
var/turf/rift_spawn_turf = get_turf(dragon)
if(isopenspaceturf(rift_spawn_turf))
@@ -64,7 +64,7 @@
light_color = LIGHT_COLOR_PURPLE
light_range = 10
anchored = TRUE
- density = FALSE
+ density = TRUE
plane = MASSIVE_OBJ_PLANE
/// The amount of time the rift has charged for.
var/time_charged = 0
@@ -77,11 +77,13 @@
/// Current charge state of the rift.
var/charge_state = CHARGE_ONGOING
/// The interval for adding additional space carp spawns to the rift.
- var/carp_interval = 60
+ var/carp_interval = 45
/// The time since an extra carp was added to the ghost role spawning pool.
var/last_carp_inc = 0
/// A list of all the ckeys which have used this carp rift to spawn in as carps.
var/list/ckey_list = list()
+ /// Gravity aura for the rift, makes all turfs nearby forced grav.
+ var/datum/proximity_monitor/advanced/gravity/warns_on_entrance/gravity_aura
/datum/armor/structure_carp_rift
energy = 100
@@ -98,14 +100,25 @@
AddComponent( \
/datum/component/aura_healing, \
- range = 0, \
+ range = 1, \
simple_heal = 5, \
limit_to_trait = TRAIT_HEALS_FROM_CARP_RIFTS, \
healing_color = COLOR_BLUE, \
)
+ gravity_aura = new(
+ /* host = */src,
+ /* range = */15,
+ /* ignore_if_not_on_turf = */TRUE,
+ /* gravity = */1,
+ )
+
START_PROCESSING(SSobj, src)
+/obj/structure/carp_rift/Destroy()
+ QDEL_NULL(gravity_aura)
+ return ..()
+
// Carp rifts always take heavy explosion damage. Discourages the use of maxcaps
// and favours more weaker explosives to destroy the portal
// as they have the same effect on the portal.
@@ -183,7 +196,7 @@
if(time_charged >= max_charge)
charge_state = CHARGE_COMPLETED
var/area/A = get_area(src)
- priority_announce("Spatial object has reached peak energy charge in [initial(A.name)], please stand-by.", "Central Command Wildlife Observations")
+ priority_announce("Spatial object has reached peak energy charge in [initial(A.name)], please stand-by.", "Central Command Wildlife Observations", has_important_message = TRUE)
atom_integrity = INFINITY
icon_state = "carp_rift_charged"
set_light_color(LIGHT_COLOR_DIM_YELLOW)
@@ -230,12 +243,13 @@
to_chat(user, span_warning("The rift already summoned enough carp!"))
return FALSE
- if(!dragon)
+ if(isnull(dragon))
return
var/mob/living/newcarp = new dragon.minion_to_spawn(loc)
newcarp.faction = dragon.owner.current.faction
newcarp.AddElement(/datum/element/nerfed_pulling, GLOB.typecache_general_bad_things_to_easily_move)
newcarp.AddElement(/datum/element/prevent_attacking_of_types, GLOB.typecache_general_bad_hostile_attack_targets, "this tastes awful!")
+ dragon.wavespeak?.link_mob(newcarp)
if(!is_listed)
ckey_list += user.ckey
diff --git a/code/modules/antagonists/space_dragon/space_dragon.dm b/code/modules/antagonists/space_dragon/space_dragon.dm
index 7ebcd4dde0c..25fb005ca61 100644
--- a/code/modules/antagonists/space_dragon/space_dragon.dm
+++ b/code/modules/antagonists/space_dragon/space_dragon.dm
@@ -21,9 +21,13 @@
/// Whether or not Space Dragon has completed their objective, and thus triggered the ending sequence.
var/objective_complete = FALSE
/// What mob to spawn from ghosts using this dragon's rifts
- var/minion_to_spawn = /mob/living/basic/carp
+ var/minion_to_spawn = /mob/living/basic/carp/advanced
/// What AI mobs to spawn from this dragon's rifts
var/ai_to_spawn = /mob/living/basic/carp
+ /// Wavespeak mind linker, to allow telepathy between dragon and carps
+ var/datum/component/mind_linker/wavespeak
+ /// What areas are we allowed to place rifts in?
+ var/list/chosen_rift_areas = list()
/datum/antagonist/space_dragon/greet()
. = ..()
@@ -33,16 +37,42 @@
Today, we will snuff out one of those lights.")
to_chat(owner, span_boldwarning("You have five minutes to find a safe location to place down the first rift. If you take longer than five minutes to place a rift, you will be returned from whence you came."))
owner.announce_objectives()
- SEND_SOUND(owner.current, sound('sound/magic/demon_attack1.ogg'))
+ owner.current.playsound_local(get_turf(owner.current), 'sound/magic/demon_attack1.ogg', 80)
/datum/antagonist/space_dragon/forge_objectives()
+ var/static/list/area/allowed_areas
+ if(!allowed_areas)
+ // Areas that will prove a challeng for the dragon and are provocative to the crew.
+ allowed_areas = typecacheof(list(
+ /area/station/command,
+ /area/station/engineering,
+ /area/station/science,
+ /area/station/security,
+ ))
+
+ var/list/possible_areas = typecache_filter_list(get_sorted_areas(), allowed_areas)
+ for(var/area/possible_area as anything in possible_areas)
+ if(initial(possible_area.outdoors) || !(possible_area.area_flags & VALID_TERRITORY))
+ possible_areas -= possible_area
+
+ for(var/i in 1 to 5)
+ chosen_rift_areas += pick_n_take(possible_areas)
+
var/datum/objective/summon_carp/summon = new
- summon.dragon = src
objectives += summon
+ summon.owner = owner
+ summon.update_explanation_text()
/datum/antagonist/space_dragon/on_gain()
forge_objectives()
rift_ability = new()
+ owner.special_role = ROLE_SPACE_DRAGON
+ owner.set_assigned_role(SSjob.GetJobType(/datum/job/space_dragon))
+ return ..()
+
+/datum/antagonist/space_dragon/on_removal()
+ owner.special_role = null
+ owner.set_assigned_role(SSjob.GetJobType(/datum/job/unassigned))
return ..()
/datum/antagonist/space_dragon/apply_innate_effects(mob/living/mob_override)
@@ -52,6 +82,15 @@
antag.faction |= FACTION_CARP
// Give the ability over if we have one
rift_ability?.Grant(antag)
+ wavespeak = antag.AddComponent( \
+ /datum/component/mind_linker, \
+ network_name = "Wavespeak", \
+ chat_color = "#635BAF", \
+ signals_which_destroy_us = list(COMSIG_LIVING_DEATH), \
+ speech_action_icon = 'icons/mob/actions/actions_space_dragon.dmi', \
+ speech_action_icon_state = "wavespeak", \
+ )
+ RegisterSignal(wavespeak, COMSIG_PARENT_QDELETING, PROC_REF(clear_wavespeak))
/datum/antagonist/space_dragon/remove_innate_effects(mob/living/mob_override)
var/mob/living/antag = mob_override || owner.current
@@ -59,11 +98,14 @@
UnregisterSignal(antag, COMSIG_LIVING_DEATH)
antag.faction -= FACTION_CARP
rift_ability?.Remove(antag)
+ QDEL_NULL(wavespeak)
/datum/antagonist/space_dragon/Destroy()
rift_list = null
carp = null
QDEL_NULL(rift_ability)
+ QDEL_NULL(wavespeak)
+ chosen_rift_areas.Cut()
return ..()
/datum/antagonist/space_dragon/get_preview_icon()
@@ -77,6 +119,10 @@
return icon
+/datum/antagonist/space_dragon/proc/clear_wavespeak()
+ SIGNAL_HANDLER
+ wavespeak = null
+
/**
* Checks to see if we need to do anything with the current state of the dragon's rifts.
*
@@ -132,11 +178,10 @@
objective_complete = TRUE
permanant_empower()
var/datum/objective/summon_carp/main_objective = locate() in objectives
- if(main_objective)
- main_objective.completed = TRUE
+ main_objective?.completed = TRUE
priority_announce("A large amount of lifeforms have been detected approaching [station_name()] at extreme speeds. \
- Remaining crew are advised to evacuate as soon as possible.", "Central Command Wildlife Observations")
- sound_to_playing_players('sound/creatures/space_dragon_roar.ogg')
+ Remaining crew are advised to evacuate as soon as possible.", "Central Command Wildlife Observations", has_important_message = TRUE)
+ sound_to_playing_players('sound/creatures/space_dragon_roar.ogg', volume = 75)
for(var/obj/structure/carp_rift/rift as anything in rift_list)
rift.carp_stored = 999999
rift.time_charged = rift.max_charge
@@ -177,8 +222,19 @@
owner.current.remove_movespeed_modifier(/datum/movespeed_modifier/dragon_rage)
/datum/objective/summon_carp
- var/datum/antagonist/space_dragon/dragon
- explanation_text = "Summon and protect the rifts to flood the station with carp."
+ explanation_text = "Summon 3 rifts in order to flood the station with carp."
+
+/datum/objective/summon_carp/update_explanation_text()
+ var/datum/antagonist/space_dragon/dragon_owner = owner.has_antag_datum(/datum/antagonist/space_dragon)
+ if(isnull(dragon_owner))
+ return
+
+ var/list/converted_names = list()
+ for(var/area/possible_area as anything in dragon_owner.chosen_rift_areas)
+ converted_names += possible_area.get_original_area_name()
+
+ explanation_text = initial(explanation_text)
+ explanation_text += " Your possible rift locations are: [english_list(converted_names)]"
/datum/antagonist/space_dragon/roundend_report()
var/list/parts = list()
@@ -197,7 +253,17 @@
parts += "The [name] was successful!"
else
parts += "The [name] has failed!"
- if(carp.len)
- parts += "The [name] was assisted by:"
- parts += printplayerlist(carp)
+
+ if(length(carp))
+ parts += "
The [name] was assisted by:"
+ parts += "