diff --git a/modular_causticcove/code/modules/chompy_vore/eating/belly_import.dm b/modular_causticcove/code/modules/chompy_vore/eating/belly_import.dm
index 75279300fef..36995bf8029 100644
--- a/modular_causticcove/code/modules/chompy_vore/eating/belly_import.dm
+++ b/modular_causticcove/code/modules/chompy_vore/eating/belly_import.dm
@@ -19,7 +19,6 @@
var/list/valid_names = list()
var/list/valid_lists = list()
var/list/updated = list()
-
for(var/list/raw_list in input_data)
if(length(valid_names) >= BELLIES_MAX)
break
@@ -787,25 +786,25 @@
new_belly.tail_extra_overlay2 = new_tail_extra_overlay2
*/
if(istext(belly_data["belly_fullscreen_color"]))
- var/new_belly_fullscreen_color = sanitize_hexcolor(belly_data["belly_fullscreen_color"],new_belly.belly_fullscreen_color)
+ var/new_belly_fullscreen_color = sanitize_hexcolor(belly_data["belly_fullscreen_color"], 6, 0, new_belly.belly_fullscreen_color)
new_belly.belly_fullscreen_color = new_belly_fullscreen_color
if(istext(belly_data["belly_fullscreen_color2"]))
- var/new_belly_fullscreen_color2 = sanitize_hexcolor(belly_data["belly_fullscreen_color2"],new_belly.belly_fullscreen_color2)
+ var/new_belly_fullscreen_color2 = sanitize_hexcolor(belly_data["belly_fullscreen_color2"], 6, 0, new_belly.belly_fullscreen_color2)
new_belly.belly_fullscreen_color2 = new_belly_fullscreen_color2
else if(istext(belly_data["belly_fullscreen_color_secondary"])) // Inter server support between virgo and chomp!
- var/new_belly_fullscreen_color2 = sanitize_hexcolor(belly_data["belly_fullscreen_color_secondary"],new_belly.belly_fullscreen_color2)
+ var/new_belly_fullscreen_color2 = sanitize_hexcolor(belly_data["belly_fullscreen_color_secondary"], 6, 0, new_belly.belly_fullscreen_color2)
new_belly.belly_fullscreen_color2 = new_belly_fullscreen_color2
if(istext(belly_data["belly_fullscreen_color3"]))
- var/new_belly_fullscreen_color3 = sanitize_hexcolor(belly_data["belly_fullscreen_color3"],new_belly.belly_fullscreen_color3)
+ var/new_belly_fullscreen_color3 = sanitize_hexcolor(belly_data["belly_fullscreen_color3"], 6, 0, new_belly.belly_fullscreen_color3)
new_belly.belly_fullscreen_color3 = new_belly_fullscreen_color3
else if(istext(belly_data["belly_fullscreen_color_trinary"])) // Inter server support between virgo and chomp!
- var/new_belly_fullscreen_color3 = sanitize_hexcolor(belly_data["belly_fullscreen_color_trinary"],new_belly.belly_fullscreen_color3)
+ var/new_belly_fullscreen_color3 = sanitize_hexcolor(belly_data["belly_fullscreen_color_trinary"], 6, 0, new_belly.belly_fullscreen_color3)
new_belly.belly_fullscreen_color3 = new_belly_fullscreen_color3
if(istext(belly_data["belly_fullscreen_color4"]))
- var/new_belly_fullscreen_color4 = sanitize_hexcolor(belly_data["belly_fullscreen_color4"],new_belly.belly_fullscreen_color4)
+ var/new_belly_fullscreen_color4 = sanitize_hexcolor(belly_data["belly_fullscreen_color4"], 6, 0,new_belly.belly_fullscreen_color4)
new_belly.belly_fullscreen_color4 = new_belly_fullscreen_color4
if(istext(belly_data["belly_fullscreen_alpha"]))
@@ -1065,11 +1064,11 @@
new_belly.reagent_mode_flags += new_belly.reagent_mode_flag_list[reagent_flag]
if(istext(belly_data["custom_reagentcolor"]))
- var/custom_reagentcolor = sanitize_hexcolor(belly_data["custom_reagentcolor"],new_belly.custom_reagentcolor)
+ var/custom_reagentcolor = sanitize_hexcolor(belly_data["custom_reagentcolor"], 6, 0, new_belly.custom_reagentcolor)
new_belly.custom_reagentcolor = custom_reagentcolor
if(istext(belly_data["mush_color"]))
- var/mush_color = sanitize_hexcolor(belly_data["mush_color"],new_belly.mush_color)
+ var/mush_color = sanitize_hexcolor(belly_data["mush_color"], 6, 0, new_belly.mush_color)
new_belly.mush_color = mush_color
if(istext(belly_data["mush_alpha"]))
diff --git a/tgui/packages/tgui/interfaces/VorePanel/VorePanelMainTabs/VoreBellySelectionAndCustomization.tsx b/tgui/packages/tgui/interfaces/VorePanel/VorePanelMainTabs/VoreBellySelectionAndCustomization.tsx
index 19b45d58074..f40232064a7 100644
--- a/tgui/packages/tgui/interfaces/VorePanel/VorePanelMainTabs/VoreBellySelectionAndCustomization.tsx
+++ b/tgui/packages/tgui/interfaces/VorePanel/VorePanelMainTabs/VoreBellySelectionAndCustomization.tsx
@@ -113,6 +113,14 @@ export const VoreBellySelectionAndCustomization = (props: {
>
)}
+ act('exportpanel')}>
+ Export
+
+
+ act('importpanel')}>
+ Import
+
+
{showSearch && (
<>
diff --git a/tgui/packages/tgui/interfaces/VorePanelExport/VorePanelExportBellyString.tsx b/tgui/packages/tgui/interfaces/VorePanelExport/VorePanelExportBellyString.tsx
new file mode 100644
index 00000000000..2a2a87b1ca4
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/VorePanelExport/VorePanelExportBellyString.tsx
@@ -0,0 +1,930 @@
+import {
+ ItemModeSpan,
+ ModeSpan,
+ STRUGGLE_OUTSIDE_ABSORBED_MESSAGE,
+ STRUGGLE_OUTSIDE_MESSAGE,
+} from './constants';
+import {
+ formatListEmotes,
+ formatListItems,
+ formatListMessages,
+ getYesNo,
+} from './functions';
+import type { Belly, EmoteEntry, SettingItem } from './types';
+
+import {
+ GetAddons,
+ GetAutotransferFlags,
+ GetLiquidAddons,
+} from './VorePanelExportBellyStringHelpers';
+
+// prettier-ignore
+export const generateBellyString = (belly: Belly, index: number) => {
+ const {
+ // General Information
+ name,
+ desc,
+ display_name,
+ message_mode,
+ absorbed_desc,
+ vore_verb,
+ release_verb,
+
+ // Controls
+ mode,
+ addons,
+ item_mode,
+
+ // Options
+ digest_brute,
+ digest_burn,
+ digest_oxy,
+ digest_tox,
+ digest_clone,
+ bellytemperature,
+ temperature_damage,
+
+ can_taste,
+ is_feedable,
+ contaminates,
+ contamination_flavor,
+ contamination_color,
+ nutrition_percent,
+ bulge_size,
+ display_absorbed_examine,
+ save_digest_mode,
+ emote_active,
+ emote_time,
+ shrink_grow_size,
+ vorespawn_blacklist,
+ vorespawn_whitelist,
+ vorespawn_absorbed,
+ egg_type,
+ egg_name,
+ selective_preference,
+ recycling,
+ storing_nutrition,
+ entrance_logs,
+ item_digest_logs,
+
+ // Messages
+ struggle_messages_outside,
+ struggle_messages_inside,
+ absorbed_struggle_messages_outside,
+ absorbed_struggle_messages_inside,
+ escape_attempt_messages_owner,
+ escape_attempt_messages_prey,
+ escape_messages_owner,
+ escape_messages_prey,
+ escape_messages_outside,
+ escape_item_messages_owner,
+ escape_item_messages_prey,
+ escape_item_messages_outside,
+ escape_fail_messages_owner,
+ escape_fail_messages_prey,
+ escape_attempt_absorbed_messages_owner,
+ escape_attempt_absorbed_messages_prey,
+ escape_absorbed_messages_owner,
+ escape_absorbed_messages_prey,
+ escape_absorbed_messages_outside,
+ escape_fail_absorbed_messages_owner,
+ escape_fail_absorbed_messages_prey,
+ primary_transfer_messages_owner,
+ primary_transfer_messages_prey,
+ secondary_transfer_messages_owner,
+ secondary_transfer_messages_prey,
+ primary_autotransfer_messages_owner,
+ primary_autotransfer_messages_prey,
+ secondary_autotransfer_messages_owner,
+ secondary_autotransfer_messages_prey,
+ digest_chance_messages_owner,
+ digest_chance_messages_prey,
+ absorb_chance_messages_owner,
+ absorb_chance_messages_prey,
+ digest_messages_owner,
+ digest_messages_prey,
+ absorb_messages_owner,
+ absorb_messages_prey,
+ unabsorb_messages_owner,
+ unabsorb_messages_prey,
+ examine_messages,
+ examine_messages_absorbed,
+ trash_eater_in,
+ trash_eater_out,
+ displayed_message_flags,
+ // emote_list,
+ emotes_digest,
+ emotes_hold,
+ emotes_holdabsorbed,
+ emotes_absorb,
+ emotes_heal,
+ emotes_drain,
+ emotes_steal,
+ emotes_egg,
+ emotes_shrink,
+ emotes_grow,
+ emotes_unabsorb,
+
+ // Sounds
+ is_wet,
+ wet_loop,
+ fancy_vore,
+ vore_sound,
+ release_sound,
+ sound_volume,
+ noise_freq,
+
+ // Visuals
+ affects_vore_sprites,
+ count_absorbed_prey_for_sprite,
+ resist_triggers_animation,
+ size_factor_for_sprite,
+ belly_sprite_to_affect,
+
+ // Visuals (Belly Fullscreens Preview and Coloring)
+ belly_fullscreen,
+ belly_fullscreen_color,
+ belly_fullscreen_color2,
+ belly_fullscreen_color3,
+ belly_fullscreen_color4,
+ belly_fullscreen_alpha,
+ colorization_enabled,
+
+ // Visuals (Vore FX)
+ disable_hud,
+
+ // Interactions
+ escapable,
+
+ escapechance,
+ escapechance_absorbed,
+ escapetime,
+
+ transferchance,
+ transferlocation,
+
+ transferchance_secondary,
+ transferlocation_secondary,
+
+ absorbchance,
+ digestchance,
+
+ belchchance,
+
+ // Interactions (Auto-Transfer)
+ autotransferwait,
+ autotransferchance,
+ autotransferlocation,
+ autotransferextralocation,
+ autotransferchance_secondary,
+ autotransferlocation_secondary,
+ autotransferextralocation_secondary,
+ autotransfer_enabled,
+ autotransfer_min_amount,
+ autotransfer_max_amount,
+ autotransfer_whitelist,
+ autotransfer_blacklist,
+ autotransfer_secondary_whitelist,
+ autotransfer_secondary_blacklist,
+ autotransfer_whitelist_items,
+ autotransfer_blacklist_items,
+ autotransfer_secondary_whitelist_items,
+ autotransfer_secondary_blacklist_items,
+
+ // Liquid Options
+ show_liquids,
+ reagentbellymode,
+ reagent_chosen,
+ reagent_name,
+ reagent_transfer_verb,
+ gen_time_display,
+ custom_max_volume,
+ vorefootsteps_sounds,
+ reagent_mode_flag_list,
+ liquid_overlay,
+ max_liquid_level,
+ reagent_touches,
+ mush_overlay,
+ mush_color,
+ mush_alpha,
+ max_mush,
+ min_mush,
+ item_mush_val,
+ custom_reagentcolor,
+ custom_reagentalpha,
+ metabolism_overlay,
+ metabolism_mush_ratio,
+ max_ingested,
+ custom_ingested_color,
+ custom_ingested_alpha,
+
+ // Liquid Messages
+ liquid_fullness1_messages,
+ liquid_fullness2_messages,
+ liquid_fullness3_messages,
+ liquid_fullness4_messages,
+ liquid_fullness5_messages,
+
+ fullness1_messages,
+ fullness2_messages,
+ fullness3_messages,
+ fullness4_messages,
+ fullness5_messages,
+ } = belly;
+
+ let result = '';
+ result += `
';
+
+ result += `
`;
+ result += '
';
+ result += `Addons:
${GetAddons(addons)}
`;
+
+ result += '
== Descriptions ==';
+
+ const infoFields = [
+ { label: 'Vore Verb', value: vore_verb },
+ { label: 'Release Verb', value: release_verb },
+ { label: 'Description', value: `"${desc}"` },
+ { label: 'Absorbed Description', value: `"${absorbed_desc}"` },
+ ];
+
+ infoFields.forEach(({ label, value }) => {
+ result += `${label}:
${value}
`;
+ });
+
+ result += '
';
+
+ result += '
== Messages ==';
+ result += `Show All Interactive Messages: ${
+ message_mode
+ ? '
Yes'
+ : 'No'
+ }
`;
+ result += ''; // Start Div messagesTabpanel
+ result += '
';
+ result += '
';
+
+ const tabLinks: {
+ id: string;
+ label: string;
+ active?: boolean;
+ enabled?: boolean;
+ }[] = [
+ {
+ id: 'escapeAttemptMessagesOwner',
+ label: 'Escape Attempt Messages (Owner)',
+ active: true,
+ },
+ {
+ id: 'escapeAttemptMessagesPrey',
+ label: 'Escape Attempt Messages (Prey)',
+ },
+ { id: 'escapeMessagesOwner', label: 'Escape Message (Owner)' },
+ { id: 'escapeMessagesPrey', label: 'Escape Message (Prey)' },
+ { id: 'escapeMessagesOutside', label: 'Escape Message (Outside)' },
+ { id: 'escapeItemMessagesOwner', label: 'Escape Item Messages (Owner)' },
+ { id: 'escapeItemMessagesPrey', label: 'Escape Item Messages (Prey)' },
+ {
+ id: 'escapeItemMessagesOutside',
+ label: 'Escape Item Messages (Outside)',
+ },
+ { id: 'escapeFailMessagesOwner', label: 'Escape Fail Messages (Owner)' },
+ { id: 'escapeFailMessagesPrey', label: 'Escape Fail Messages (Prey)' },
+ {
+ id: 'escapeAttemptAbsorbedMessagesOwner',
+ label: 'Escape Attempt Absorbed Messages (Owner)',
+ },
+ {
+ id: 'escapeAttemptAbsorbedMessagesPrey',
+ label: 'Escape Attempt Absorbed Messages (Prey)',
+ },
+ {
+ id: 'escapeAbsorbedMessagesOwner',
+ label: 'Escape Absorbed Messages (Owner)',
+ },
+ {
+ id: 'escapeAbsorbedMessagesPrey',
+ label: 'Escape Absorbed Messages (Prey)',
+ },
+ {
+ id: 'escapeAbsorbedMessagesOutside',
+ label: 'Escape Absorbed Messages (Outside)',
+ },
+ {
+ id: 'escapeFailAbsorbedMessagesOwner',
+ label: 'Escape Fail Absorbed Messages (Owner)',
+ },
+ {
+ id: 'escapeFailAbsorbedMessagesPrey',
+ label: 'Escape Fail Absorbed Messages (Prey)',
+ },
+ {
+ id: 'primaryTransferMessagesOwner',
+ label: 'Primary Transfer Messages (Owner)',
+ },
+ {
+ id: 'primaryTransferMessagesPrey',
+ label: 'Primary Transfer Messages (Prey)',
+ },
+ {
+ id: 'secondaryTransferMessagesOwner',
+ label: 'Secondary Transfer Messages (Owner)',
+ },
+ {
+ id: 'secondaryTransferMessagesPrey',
+ label: 'Secondary Transfer Messages (Prey)',
+ },
+ {
+ id: 'digestChanceMessagesOwner',
+ label: 'Digest Chance Messages (Owner)',
+ },
+ { id: 'digestChanceMessagesPrey', label: 'Digest Chance Messages (Prey)' },
+ {
+ id: 'absorbChanceMessagesOwner',
+ label: 'Absorb Chance Messages (Owner)',
+ },
+ { id: 'absorbChanceMessagesPrey', label: 'Absorb Chance Messages (Prey)' },
+ {
+ id: 'struggleMessagesOutside',
+ label: 'Struggle Messages (Outside)',
+ enabled: (displayed_message_flags & STRUGGLE_OUTSIDE_MESSAGE) > 0,
+ },
+ { id: 'struggleMessagesInside', label: 'Struggle Messages (Inside)' },
+ {
+ id: 'absorbedStruggleOutside',
+ label: 'Absorbed Struggle Messages (Outside)',
+ enabled:
+ (displayed_message_flags & STRUGGLE_OUTSIDE_ABSORBED_MESSAGE) > 0,
+ },
+ {
+ id: 'absorbedStruggleInside',
+ label: 'Absorbed Struggle Messages (Inside)',
+ },
+ { id: 'digestMessagesOwner', label: 'Digest Messages (Owner)' },
+ { id: 'digestMessagesPrey', label: 'Digest Messages (Prey)' },
+ { id: 'absorbMessagesOwner', label: 'Absorb Messages (Owner)' },
+ { id: 'absorbMessagesPrey', label: 'Absorb Messages (Prey)' },
+ { id: 'unabsorbMessagesOwner', label: 'Unabsorb Messages (Owner)' },
+ { id: 'unabsorbMessagesPrey', label: 'Unabsorb Messages (Prey)' },
+ { id: 'examineMessages', label: 'Examine Messages' },
+ { id: 'examineMessagesAbsorbed', label: 'Examine Messages (Absorbed)' },
+ { id: 'trash_eater_in', label: 'Trash Eater Ingest Messages' },
+ { id: 'trash_eater_out', label: 'Item Expel Messages' },
+ ];
+
+ tabLinks.forEach(({ id, label, enabled }, i) => {
+ const isActive = i === 0 ? ' active' : '';
+ const status = enabled === undefined ? '' : getYesNo(enabled);
+
+ result += `
${label}${status}`;
+ });
+
+ result += '
';
+
+ result += '
';
+ result += '
';
+
+ const messageTypes: [string, string[] | null][] = [
+ ['escapeAttemptMessagesOwner', escape_attempt_messages_owner],
+ ['escapeAttemptMessagesPrey', escape_attempt_messages_prey],
+ ['escapeMessagesOwner', escape_messages_owner],
+ ['escapeMessagesPrey', escape_messages_prey],
+ ['escapeMessagesOutside', escape_messages_outside],
+ ['escapeItemMessagesOwner', escape_item_messages_owner],
+ ['escapeItemMessagesPrey', escape_item_messages_prey],
+ ['escapeItemMessagesOutside', escape_item_messages_outside],
+ ['escapeFailMessagesOwner', escape_fail_messages_owner],
+ ['escapeFailMessagesPrey', escape_fail_messages_prey],
+ [
+ 'escapeAttemptAbsorbedMessagesOwner',
+ escape_attempt_absorbed_messages_owner,
+ ],
+ [
+ 'escapeAttemptAbsorbedMessagesPrey',
+ escape_attempt_absorbed_messages_prey,
+ ],
+ ['escapeAbsorbedMessagesOwner', escape_absorbed_messages_owner],
+ ['escapeAbsorbedMessagesPrey', escape_absorbed_messages_prey],
+ ['escapeAbsorbedMessagesOutside', escape_absorbed_messages_outside],
+ ['escapeFailAbsorbedMessagesOwner', escape_fail_absorbed_messages_owner],
+ ['escapeFailAbsorbedMessagesPrey', escape_fail_absorbed_messages_prey],
+ ['primaryTransferMessagesOwner', primary_transfer_messages_owner],
+ ['primaryTransferMessagesPrey', primary_transfer_messages_prey],
+ ['secondaryTransferMessagesOwner', secondary_transfer_messages_owner],
+ ['secondaryTransferMessagesPrey', secondary_transfer_messages_prey],
+ ['primaryAutoTransferMessagesOwner', primary_autotransfer_messages_owner],
+ ['primaryAutoTransferMessagesPrey', primary_autotransfer_messages_prey],
+ [
+ 'secondaryAutoTransferMessagesOwner',
+ secondary_autotransfer_messages_owner,
+ ],
+ ['secondaryAutoTransferMessagesPrey', secondary_autotransfer_messages_prey],
+ ['digestChanceMessagesOwner', digest_chance_messages_owner],
+ ['digestChanceMessagesPrey', digest_chance_messages_prey],
+ ['absorbChanceMessagesOwner', absorb_chance_messages_owner],
+ ['absorbChanceMessagesPrey', absorb_chance_messages_prey],
+ ['struggleMessagesOutside', struggle_messages_outside],
+ ['struggleMessagesInside', struggle_messages_inside],
+ ['absorbedStruggleOutside', absorbed_struggle_messages_outside],
+ ['absorbedStruggleInside', absorbed_struggle_messages_inside],
+ ['digestMessagesOwner', digest_messages_owner],
+ ['digestMessagesPrey', digest_messages_prey],
+ ['absorbMessagesOwner', absorb_messages_owner],
+ ['absorbMessagesPrey', absorb_messages_prey],
+ ['unabsorbMessagesOwner', unabsorb_messages_owner],
+ ['unabsorbMessagesPrey', unabsorb_messages_prey],
+ ['examineMessages', examine_messages],
+ ['examineMessagesAbsorbed', examine_messages_absorbed],
+ ['trash_eater_in', trash_eater_in],
+ ['trash_eater_out', trash_eater_out],
+ ];
+
+ messageTypes.forEach(([messageKey, messageData], i) => {
+ result += formatListMessages(`${messageKey}${index}`, messageData, i === 0);
+ });
+ result += '
';
+ result += '
';
+ result += '
'; // End Div messagesTabpanel
+
+ result += '= Idle Messages =
';
+
+ const emoteSections: EmoteEntry[] = [
+ { label: 'Idle Messages (Hold)', messages: emotes_hold },
+ { label: 'Idle Messages (Hold Absorbed)', messages: emotes_holdabsorbed },
+ { label: 'Idle Messages (Digest)', messages: emotes_digest },
+ { label: 'Idle Messages (Absorb)', messages: emotes_absorb },
+ { label: 'Idle Messages (Unabsorb)', messages: emotes_unabsorb },
+ { label: 'Idle Messages (Drain)', messages: emotes_drain },
+ { label: 'Idle Messages (Heal)', messages: emotes_heal },
+ { label: 'Idle Messages (Size Steal)', messages: emotes_steal },
+ { label: 'Idle Messages (Shrink)', messages: emotes_shrink },
+ { label: 'Idle Messages (Grow)', messages: emotes_grow },
+ { label: 'Idle Messages (Encase In Egg)', messages: emotes_egg },
+ ];
+
+ result += formatListEmotes(emoteSections);
+
+ result += '
';
+
+ result += '
';
+
+ result += ``;
+
+ // OPTIONS
+
+ result += '
';
+ result += `';
+
+ result += `
`;
+ result += '
';
+ result += '
';
+
+ const settingItem: SettingItem[] = [
+ {
+ label: 'Can Taste',
+ value: can_taste,
+ formatter: (val: boolean): string => getYesNo(val),
+ },
+ {
+ label: 'Feedable',
+ value: is_feedable,
+ formatter: (val: boolean): string => getYesNo(val),
+ },
+ {
+ label: 'Contaminates',
+ value: contaminates,
+ formatter: (val: boolean): string => getYesNo(val),
+ },
+ { label: 'Contamination Flavor', value: contamination_flavor },
+ { label: 'Contamination Color', value: contamination_color },
+ {
+ label: 'Nutritional Gain',
+ value: nutrition_percent,
+ formatter: (val: number): string => `${val}%`,
+ },
+ {
+ label: 'Required Examine Size',
+ value: bulge_size,
+ formatter: (val: number): string => `${val * 100}%`,
+ },
+ {
+ label: 'Display Absorbed Examines',
+ value: display_absorbed_examine,
+ formatter: (val: boolean): string =>
+ val
+ ? 'True'
+ : 'False',
+ },
+ {
+ label: 'Save Digest Mode',
+ value: save_digest_mode,
+ formatter: (val: boolean): string =>
+ val
+ ? 'True'
+ : 'False',
+ },
+ {
+ label: 'Idle Emotes',
+ value: emote_active,
+ formatter: (val: boolean): string =>
+ val
+ ? 'Active'
+ : 'Inactive',
+ },
+ {
+ label: 'Idle Emote Delay',
+ value: emote_time,
+ formatter: (val: number): string => `${val} seconds`,
+ },
+ {
+ label: 'Shrink/Grow Size',
+ value: shrink_grow_size,
+ formatter: (val: number): string => `${val * 100}%`,
+ },
+ {
+ label: 'Vore Spawn Blacklist',
+ value: vorespawn_blacklist,
+ formatter: (val: boolean): string => getYesNo(val),
+ },
+ {
+ label: 'Vore Spawn Whitelist',
+ value: vorespawn_whitelist,
+ formatter: (val: string[]): string =>
+ val.length ? val.join(', ') : 'Anyone!',
+ },
+ {
+ label: 'Vore Spawn Absorbed',
+ value: vorespawn_absorbed,
+ formatter: (val: number): string =>
+ val === 0
+ ? 'No'
+ : val === 1
+ ? 'Yes'
+ : 'Prey Choice',
+ },
+ { label: 'Egg Type', value: egg_type },
+ { label: 'Selective Mode Preference', value: selective_preference },
+ ];
+
+ result += formatListItems(settingItem);
+
+ result += '
';
+ result += '
';
+
+ // END OPTIONS
+ // SOUNDS
+
+ result += '
';
+ result += `';
+
+ result += `
`;
+ result += '
';
+ result += '
';
+
+ const soundItems = [
+ { label: 'Fleshy Belly', value: is_wet, formatter: getYesNo },
+ { label: 'Internal Loop', value: wet_loop, formatter: getYesNo },
+ { label: 'Use Fancy Sounds', value: fancy_vore, formatter: getYesNo },
+ { label: 'Vore Sound', value: vore_sound },
+ { label: 'Release Sound', value: release_sound },
+ ];
+
+ result += formatListItems(soundItems);
+
+ result += '
';
+ result += '
';
+
+ // END SOUNDS
+ // VISUALS
+
+ result += '
';
+ result += `';
+
+ result += `
';
+ result += '
Vore Sprites';
+ result += '
';
+
+ const visualItems = [
+ {
+ label: 'Affect Vore Sprites',
+ value: affects_vore_sprites,
+ formatter: getYesNo,
+ },
+ {
+ label: 'Count Absorbed prey for vore sprites',
+ value: count_absorbed_prey_for_sprite,
+ formatter: getYesNo,
+ },
+ {
+ label: 'Animation when prey resist',
+ value: resist_triggers_animation,
+ formatter: getYesNo,
+ },
+ { label: 'Vore Sprite Size Factor', value: size_factor_for_sprite },
+ { label: 'Belly Sprite to affect', value: belly_sprite_to_affect },
+ ];
+
+ result += formatListItems(visualItems);
+
+ result += '
';
+ result += '
Belly Fullscreens Preview and Coloring';
+ result += '
';
+
+ const bellyColorItems = [
+ { label: 'Fullscreen Color', value: belly_fullscreen_color },
+ { label: 'Fullscreen Color 2', value: belly_fullscreen_color2 },
+ { label: 'Fullscreen Color 3', value: belly_fullscreen_color3 },
+ { label: 'Fullscreen Color 4', value: belly_fullscreen_color4 },
+ { label: 'Fullscreen Alpha', value: belly_fullscreen_alpha },
+ ];
+
+ bellyColorItems.forEach(({ label, value }) => {
+ const isColorCode = typeof value === 'string' && value.startsWith('#');
+ result += `- ${label}: ${
+ isColorCode ? `${value}` : value
+ }
`;
+ });
+
+ result += '
';
+ result += '
Vore FX';
+ result += '
';
+ result += `- Disable Prey HUD: ${disable_hud ? 'Yes' : 'No'}
`;
+ result += '
';
+ result += '
';
+
+ // END VISUALS
+ // INTERACTIONS
+
+ result += '';
+ result += `';
+
+ result += `
`;
+ result += '
';
+ result += `
Belly Interactions (${
+ escapable
+ ? 'Enabled'
+ : 'Disabled'
+ })`;
+ result += '
';
+
+ const interactionItems = [
+ { label: 'Escape Chance', value: escapechance, suffix: '%' },
+ {
+ label: 'Escape Chance (Absorbed)',
+ value: escapechance_absorbed,
+ suffix: '%',
+ },
+ { label: 'Escape Time', value: escapetime / 10, suffix: 's' },
+ { label: 'Transfer Chance', value: transferchance, suffix: '%' },
+ { label: 'Transfer Location', value: transferlocation },
+ {
+ label: 'Secondary Transfer Chance',
+ value: transferchance_secondary,
+ suffix: '%',
+ },
+ { label: 'Secondary Transfer Location', value: transferlocation_secondary },
+ { label: 'Absorb Chance', value: absorbchance, suffix: '%' },
+ { label: 'Digest Chance', value: digestchance, suffix: '%' },
+ { label: 'Belch Chance', value: belchchance, suffix: '%' },
+ ];
+
+ result += formatListItems(interactionItems);
+
+ result += '
';
+ result += '
';
+ result += `
Auto-Transfer Options (${
+ autotransfer_enabled
+ ? 'Enabled'
+ : 'Disabled'
+ })`;
+ result += '
';
+
+ const transferItems = [
+ { label: 'Auto-Transfer Time', value: autotransferwait / 10, suffix: 's' },
+ { label: 'Auto-Transfer Chance', value: autotransferchance, suffix: '%' },
+ { label: 'Auto-Transfer Location', value: autotransferlocation },
+ {
+ label: 'Auto-Transfer Chance (Secondary)',
+ value: autotransferchance_secondary,
+ suffix: '%',
+ },
+ {
+ label: 'Auto-Transfer Location (Secondary)',
+ value: autotransferlocation_secondary,
+ },
+ { label: 'Auto-Transfer Min Amount', value: autotransfer_min_amount },
+ { label: 'Auto-Transfer Max Amount', value: autotransfer_max_amount },
+
+ {
+ label: 'Auto-Transfer Primary Chance',
+ value: autotransferchance,
+ suffix: '%',
+ },
+ { label: 'Auto-Transfer Primary Location', value: autotransferlocation },
+ {
+ label: 'Auto-Transfer Primary Location Extras',
+ value: autotransferextralocation.join(', '),
+ },
+
+ {
+ label: 'Auto-Transfer Primary Whitelist (Mobs)',
+ value: GetAutotransferFlags(autotransfer_whitelist, true),
+ },
+ {
+ label: 'Auto-Transfer Primary Whitelist (Items)',
+ value: GetAutotransferFlags(autotransfer_whitelist_items, true),
+ },
+ {
+ label: 'Auto-Transfer Primary Blacklist (Mobs)',
+ value: GetAutotransferFlags(autotransfer_blacklist, false),
+ },
+ {
+ label: 'Auto-Transfer Primary Blacklist (Items)',
+ value: GetAutotransferFlags(autotransfer_blacklist_items, false),
+ },
+
+ {
+ label: 'Auto-Transfer Secondary Chance',
+ value: autotransferchance_secondary,
+ suffix: '%',
+ },
+ {
+ label: 'Auto-Transfer Secondary Location',
+ value: autotransferlocation_secondary,
+ },
+ {
+ label: 'Auto-Transfer Secondary Location Extras',
+ value: autotransferextralocation_secondary.join(', '),
+ },
+
+ {
+ label: 'Auto-Transfer Secondary Whitelist (Mobs)',
+ value: GetAutotransferFlags(autotransfer_secondary_whitelist, true),
+ },
+ {
+ label: 'Auto-Transfer Secondary Whitelist (Items)',
+ value: GetAutotransferFlags(autotransfer_secondary_whitelist_items, true),
+ },
+ {
+ label: 'Auto-Transfer Secondary Blacklist (Mobs)',
+ value: GetAutotransferFlags(autotransfer_secondary_blacklist, false),
+ },
+ {
+ label: 'Auto-Transfer Secondary Blacklist (Items)',
+ value: GetAutotransferFlags(
+ autotransfer_secondary_blacklist_items,
+ false,
+ ),
+ },
+ ];
+
+ result += formatListItems(transferItems);
+
+ result += '
';
+ result += '
';
+
+ // END INTERACTIONS
+ // LIQUID OPTIONS
+
+ result += '';
+ result += `';
+
+ result += `
`;
+ result += '
';
+ result += '
';
+ result += `- Generate Liquids: ${
+ reagentbellymode
+ ? 'On'
+ : 'Off'
+ }
`;
+
+ const liquidItems = [
+ { label: 'Liquid Type', value: reagent_chosen },
+ { label: 'Liquid Name', value: reagent_name },
+ { label: 'Transfer Verb', value: reagent_transfer_verb },
+ { label: 'Generation Time', value: gen_time_display },
+ { label: 'Liquid Capacity', value: custom_max_volume },
+ ];
+
+ result += formatListItems(liquidItems);
+
+ result += `- Slosh Sounds: ${
+ vorefootsteps_sounds
+ ? 'On'
+ : 'Off'
+ }
`;
+ result += `- Liquid Addons: ${GetLiquidAddons(reagent_mode_flag_list)}
`;
+ result += '
';
+ result += '
';
+
+ // END LIQUID OPTIONS
+ // LIQUID MESSAGES
+
+ result += '';
+ result += `';
+
+ result += `
`;
+ result += '
';
+
+ result += '
'; // Start Div liquidMessagesTabpanel
+ result += '
';
+ result +=
+ '
';
+
+ const fullnessItems = [
+ ['examineMessage0_20', liquid_fullness1_messages, '0 to 20%'],
+ ['examineMessage20_40', liquid_fullness2_messages, '20 to 40%'],
+ ['examineMessage40_60', liquid_fullness3_messages, '40 to 60%'],
+ ['examineMessage60_80', liquid_fullness4_messages, '60 to 80%'],
+ ['examineMessage80_100', liquid_fullness5_messages, '80 to 100%'],
+ ];
+
+ fullnessItems.forEach(([idPrefix, messages, label], i) => {
+ const activeClass = i === 0 ? 'active' : '';
+ const isOn = messages
+ ? '
On'
+ : '
Off';
+ result += `
Examine Message (${label}) (${isOn})`;
+ });
+
+ result += '
';
+
+ result += '
';
+ result += '
';
+
+ const fullnessMessages: [string, string[]][] = [
+ ['examineMessage0_20', fullness1_messages],
+ ['examineMessage20_40', fullness2_messages],
+ ['examineMessage40_60', fullness3_messages],
+ ['examineMessage60_80', fullness4_messages],
+ ['examineMessage80_100', fullness5_messages],
+ ];
+
+ fullnessMessages.forEach(([idPrefix, messages], i) => {
+ const classes = i === 0 ? 'tab-pane fade show active' : 'tab-pane fade';
+ result += `
`;
+ messages?.forEach((msg) => {
+ result += `${msg}
`;
+ });
+ result += '
';
+ });
+
+ result += '
';
+ result += '
';
+ result += '
'; // End Div liquidMessagesTabpanel
+
+ result += '
';
+
+ // END LIQUID MESSAGES
+
+ result += ' ';
+
+ return result;
+};
diff --git a/tgui/packages/tgui/interfaces/VorePanelExport/VorePanelExportBellyStringHelpers.tsx b/tgui/packages/tgui/interfaces/VorePanelExport/VorePanelExportBellyStringHelpers.tsx
new file mode 100644
index 00000000000..0cf85fdedc8
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/VorePanelExport/VorePanelExportBellyStringHelpers.tsx
@@ -0,0 +1,58 @@
+import type { BooleanLike } from 'tgui-core/react';
+
+import { AddonIcon, AutotransferFlagIcon, ReagentAddonIcon } from './constants';
+
+export const GetAddons = (addons: string[]) => {
+ const result: string[] = [];
+
+ addons?.forEach((addon) => {
+ result.push(
+ `${addon}`,
+ );
+ });
+
+ if (result.length === 0) {
+ result.push('No Addons Set');
+ }
+
+ return result;
+};
+
+export const GetLiquidAddons = (addons: string[]) => {
+ const result: string[] = [];
+
+ addons?.forEach((addon) => {
+ result.push(
+ `${addon}`,
+ );
+ });
+
+ if (result.length === 0) {
+ result.push('No Addons Set');
+ }
+
+ return result;
+};
+
+export const GetAutotransferFlags = (
+ addons: string[],
+ whitelist: BooleanLike,
+) => {
+ const result: string[] = [];
+
+ addons?.forEach((addon) => {
+ result.push(
+ `${addon}`,
+ );
+ });
+
+ if (result.length === 0) {
+ if (whitelist) {
+ result.push('Everything');
+ } else {
+ result.push('Nothing');
+ }
+ }
+
+ return result;
+};
diff --git a/tgui/packages/tgui/interfaces/VorePanelExport/VorePanelExportDownload.tsx b/tgui/packages/tgui/interfaces/VorePanelExport/VorePanelExportDownload.tsx
new file mode 100644
index 00000000000..7fb25855196
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/VorePanelExport/VorePanelExportDownload.tsx
@@ -0,0 +1,89 @@
+import { useBackend } from 'tgui/backend';
+
+import type { Data } from './types';
+import { generateBellyString } from './VorePanelExportBellyString';
+import { generateSoulcatcherString } from './VorePanelExportSoulcatcherString';
+import { getCurrentTimestamp } from './VorePanelExportTimestamp';
+
+export const downloadPrefs = (extension: string) => {
+ const { data } = useBackend();
+
+ const { db_version, db_repo, mob_name, bellies, soulcatcher } = data;
+
+ if (!bellies) {
+ return;
+ }
+
+ const validBellies = bellies.filter((belly) => !belly.prevent_saving);
+
+ const datesegment = getCurrentTimestamp();
+
+ const filename = mob_name + datesegment + extension;
+ let blob;
+
+ if (extension === '.html') {
+ const style = '';
+
+ blob = new Blob(
+ [
+ '' +
+ '' +
+ '' +
+ '' +
+ validBellies.length +
+ ' Exported Bellies (DB_VER: ' +
+ db_repo +
+ '-' +
+ db_version +
+ ')' +
+ '' +
+ '' +
+ style +
+ 'Bellies of ' +
+ mob_name +
+ '
Generated on: ' +
+ datesegment +
+ '
',
+ ],
+ {
+ type: 'text/html',
+ },
+ );
+ validBellies.forEach((belly, i) => {
+ blob = new Blob([blob, generateBellyString(belly, i)], {
+ type: 'text/html',
+ });
+ });
+ if (soulcatcher) {
+ blob = new Blob([blob, generateSoulcatcherString(soulcatcher)], {
+ type: 'text/html',
+ });
+ }
+ blob = new Blob(
+ [
+ blob,
+ '
',
+ '',
+ '
',
+ ],
+ { type: 'text/html' },
+ );
+ }
+
+ const exportPayload = {
+ [mob_name]: {
+ version: db_version,
+ repo: db_repo,
+ bellies: validBellies,
+ soulcatcher: soulcatcher,
+ },
+ };
+
+ if (extension === '.vrdb') {
+ blob = new Blob([JSON.stringify(exportPayload)], {
+ type: 'application/json',
+ });
+ }
+
+ Byond.saveBlob(blob, filename, extension);
+};
diff --git a/tgui/packages/tgui/interfaces/VorePanelExport/VorePanelExportSoulcatcherString.tsx b/tgui/packages/tgui/interfaces/VorePanelExport/VorePanelExportSoulcatcherString.tsx
new file mode 100644
index 00000000000..9cf46336872
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/VorePanelExport/VorePanelExportSoulcatcherString.tsx
@@ -0,0 +1,60 @@
+import { SoulcatcherSettingsFlag } from './constants';
+import type { Soulcatcher } from './types';
+
+// prettier-ignore
+export const generateSoulcatcherString = (soulcatcher: Soulcatcher) => {
+ const {
+ name,
+ inside_flavor,
+ capture_message,
+ transit_message,
+ release_message,
+ transfer_message,
+ delete_message,
+ linked_belly,
+ setting_flags,
+ } = soulcatcher;
+
+ const index = 'sc_1';
+
+ let result = '';
+ result += `';
+
+ result += `
`;
+ result += '
';
+
+ result += '== Settings ==
';
+ const arr: string[] = [];
+ let parsedFlag = setting_flags;
+ while (parsedFlag !== 0) {
+ arr.push(String(parsedFlag));
+ parsedFlag = Math.floor(parsedFlag / 2);
+ }
+ for (const flag in SoulcatcherSettingsFlag) {
+ if (arr.includes(flag)) {
+ result += `${SoulcatcherSettingsFlag[flag]}`;
+ } else {
+ result += `${SoulcatcherSettingsFlag[flag]}`;
+ }
+ }
+
+ result += '
';
+ result += '== Descriptions ==
';
+ result += `Inside Flavor:
${inside_flavor}
`;
+ result += `Capture Message:
${capture_message}
`;
+ result += `Transit Message:
${transit_message}
`;
+ result += `Release Message:
${release_message}
`;
+ result += `Transfer Message:
${transfer_message}
`;
+ result += `Delete Message:
${delete_message}
`;
+ result += `Linked Belly:
${linked_belly}
`;
+
+ result += '';
+ result += '
'; // End Div messagesTabpanel
+
+ result += '';
+
+ return result;
+};
diff --git a/tgui/packages/tgui/interfaces/VorePanelExport/VorePanelExportTimestamp.tsx b/tgui/packages/tgui/interfaces/VorePanelExport/VorePanelExportTimestamp.tsx
new file mode 100644
index 00000000000..f1edfae687e
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/VorePanelExport/VorePanelExportTimestamp.tsx
@@ -0,0 +1,34 @@
+export const getCurrentTimestamp = (): string => {
+ const now = new Date();
+ let hours = String(now.getHours());
+ if (hours.length < 2) {
+ hours = `0${hours}`;
+ }
+ let minutes = String(now.getMinutes());
+ if (minutes.length < 2) {
+ minutes = `0${minutes}`;
+ }
+ let dayofmonth = String(now.getDate());
+ if (dayofmonth.length < 2) {
+ dayofmonth = `0${dayofmonth}`;
+ }
+ let month = String(now.getMonth() + 1); // 0-11
+ if (month.length < 2) {
+ month = `0${month}`;
+ }
+ const year = String(now.getFullYear());
+
+ return (
+ '_' +
+ year +
+ '-' +
+ month +
+ '-' +
+ dayofmonth +
+ '_(' +
+ hours +
+ '_' +
+ minutes +
+ ')'
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/VorePanelExport/constants.ts b/tgui/packages/tgui/interfaces/VorePanelExport/constants.ts
new file mode 100644
index 00000000000..bb2e00117d0
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/VorePanelExport/constants.ts
@@ -0,0 +1,79 @@
+export const ModeSpan = {
+ Hold: 'Hold',
+ Digest: 'Digest',
+ Absorb: 'Absorb',
+ Drain: 'Drain',
+ Selective: 'Selective',
+ Unabsorb: 'Unabsorb',
+ Heal: 'Heal',
+ Shrink: 'Shrink',
+ Grow: 'Grow',
+ 'Size Steal': 'Size Steal',
+ 'Encase In Egg': 'Encase In Egg',
+};
+
+export const ItemModeSpan = {
+ Hold: 'Item: Hold',
+ 'Digest (Food Only)':
+ 'Item: Digest (Food Only)',
+ Digest: 'Item: Digest',
+ 'Digest (Dispersed Damage)':
+ 'Item: Digest (Dispersed Damage)',
+};
+
+export const AddonIcon = {
+ Numbing: '',
+ Stripping: '',
+ 'Leave Remains': '',
+ Muffles: 'bi-volume-mute',
+ 'Affect Worn Items': '',
+ 'Jams Sensors': 'bi-wifi-off',
+ 'Complete Absorb': '',
+};
+
+export const ReagentAddonIcon = {
+ 'Produce Liquids': '',
+ 'Digestion Liquids': '',
+ 'Absorption Liquids': '',
+ 'Draining Liquids': '',
+};
+
+export const AutotransferFlagIcon = {
+ Creatures: '',
+ Absorbed: '',
+ Carbon: '',
+ Silicon: '',
+ Mobs: '',
+ Animals: '',
+ Mice: '',
+ Dead: '',
+ 'Digestable Creatures': '',
+ 'Absorbable Creatures': '',
+ 'Full Health': '',
+ Items: '',
+ Trash: '',
+ Eggs: '',
+ Remains: '',
+ 'Indigestible Items': '',
+ 'Recyclable Items': '',
+ Ores: '',
+ 'Clothes and Bags': '',
+ Food: '',
+};
+
+export const SoulcatcherSettingsFlag = {
+ '1': 'Catch User',
+ '2': 'Catch Prey',
+ '4': 'Ext. hearing',
+ '8': 'Ext. Vision',
+ '16': 'Mind Backup',
+ '32': 'SR Projecting',
+ '64': 'Catch Ghost',
+ '128': 'Soulcatcher On',
+ '256': 'Show SFX',
+ '512': 'Catch Drain',
+ '1024': 'SR Vision',
+};
+
+export const STRUGGLE_OUTSIDE_MESSAGE = 0x1;
+export const STRUGGLE_OUTSIDE_ABSORBED_MESSAGE = 0x2;
diff --git a/tgui/packages/tgui/interfaces/VorePanelExport/functions.ts b/tgui/packages/tgui/interfaces/VorePanelExport/functions.ts
new file mode 100644
index 00000000000..cce871db50d
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/VorePanelExport/functions.ts
@@ -0,0 +1,51 @@
+import type { BooleanLike } from 'tgui-core/react';
+import type { EmoteEntry } from './types';
+
+export function formatListItems(
+ items: {
+ label: string;
+ value: string | string[] | BooleanLike;
+ formatter?: (val: boolean) => string;
+ suffix?: string;
+ }[],
+): string {
+ let result = '';
+ items.forEach(({ label, value, formatter, suffix = '' }) => {
+ const displayValue = formatter ? formatter(!!value) : value;
+ result += `${label}: ${displayValue}${suffix}`;
+ });
+ return result;
+}
+
+export function formatListMessages(
+ id: string,
+ messages: string[] | null,
+ isActive = false,
+): string {
+ const activeClass = isActive ? 'show active' : '';
+ let result = ``;
+ messages?.forEach((msg) => {
+ result += `${msg}
`;
+ });
+ result += '
';
+ return result;
+}
+
+export function formatListEmotes(sections: EmoteEntry[]): string {
+ let result = '';
+ sections.forEach(({ label, messages }) => {
+ if (!messages?.length) return;
+ result += `${label}:
`;
+ messages.forEach((msg) => {
+ result += `${msg}
`;
+ });
+ result += '
';
+ });
+ return result;
+}
+
+export function getYesNo(val: boolean): string {
+ return val
+ ? 'Yes'
+ : 'No';
+}
diff --git a/tgui/packages/tgui/interfaces/VorePanelExport/index.tsx b/tgui/packages/tgui/interfaces/VorePanelExport/index.tsx
new file mode 100644
index 00000000000..ab459191583
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/VorePanelExport/index.tsx
@@ -0,0 +1,29 @@
+import { Window } from 'tgui/layouts';
+import { Button, Section } from 'tgui-core/components';
+
+import { downloadPrefs } from './VorePanelExportDownload';
+
+export const VorePanelExport = () => {
+ return (
+
+
+
+
+
+ );
+};
+
+const VorePanelExportContent = (props) => {
+ return (
+
+
+
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/VorePanelExport/types.ts b/tgui/packages/tgui/interfaces/VorePanelExport/types.ts
new file mode 100644
index 00000000000..8370ff9c765
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/VorePanelExport/types.ts
@@ -0,0 +1,250 @@
+import type { BooleanLike } from 'tgui-core/react';
+
+export type Data = {
+ db_version: string;
+ db_repo: string;
+ mob_name: string;
+ bellies?: Belly[];
+ soulcatcher?: Soulcatcher;
+};
+
+export type Belly = {
+ // General Information
+ name: string;
+ desc: string;
+ display_name: string;
+ message_mode: BooleanLike;
+ absorbed_desc: string;
+ vore_verb: string;
+ release_verb: string;
+ prevent_saving: BooleanLike;
+
+ // Controls
+ mode: string;
+ addons: string[];
+ item_mode: string;
+
+ // Options
+ digest_brute: number;
+ digest_burn: number;
+ digest_oxy: number;
+ digest_tox: number;
+ digest_clone: number;
+ bellytemperature: number;
+
+ can_taste: BooleanLike;
+ is_feedable: BooleanLike;
+ contaminates: BooleanLike;
+ contamination_flavor: string;
+ contamination_color: string;
+ nutrition_percent: number;
+ bulge_size: number;
+ display_absorbed_examine: BooleanLike;
+ save_digest_mode: BooleanLike;
+ emote_active: BooleanLike;
+ emote_time: number;
+ shrink_grow_size: number;
+ vorespawn_blacklist: BooleanLike;
+ vorespawn_whitelist: string[];
+ vorespawn_absorbed: number;
+ egg_type: string;
+ egg_name: string;
+ selective_preference: string;
+ recycling: BooleanLike;
+ storing_nutrition: BooleanLike;
+ entrance_logs: BooleanLike;
+ item_digest_logs: BooleanLike;
+ temperature_damage: BooleanLike;
+
+ // Messages
+ struggle_messages_outside: string[];
+ struggle_messages_inside: string[];
+ absorbed_struggle_messages_outside: string[];
+ absorbed_struggle_messages_inside: string[];
+ escape_attempt_messages_owner: string[];
+ escape_attempt_messages_prey: string[];
+ escape_messages_owner: string[];
+ escape_messages_prey: string[];
+ escape_messages_outside: string[];
+ escape_item_messages_owner: string[];
+ escape_item_messages_prey: string[];
+ escape_item_messages_outside: string[];
+ escape_fail_messages_owner: string[];
+ escape_fail_messages_prey: string[];
+ escape_attempt_absorbed_messages_owner: string[];
+ escape_attempt_absorbed_messages_prey: string[];
+ escape_absorbed_messages_owner: string[];
+ escape_absorbed_messages_prey: string[];
+ escape_absorbed_messages_outside: string[];
+ escape_fail_absorbed_messages_owner: string[];
+ escape_fail_absorbed_messages_prey: string[];
+ primary_transfer_messages_owner: string[];
+ primary_transfer_messages_prey: string[];
+ secondary_transfer_messages_owner: string[];
+ secondary_transfer_messages_prey: string[];
+ primary_autotransfer_messages_owner: string[];
+ primary_autotransfer_messages_prey: string[];
+ secondary_autotransfer_messages_owner: string[];
+ secondary_autotransfer_messages_prey: string[];
+ digest_chance_messages_owner: string[];
+ digest_chance_messages_prey: string[];
+ absorb_chance_messages_owner: string[];
+ absorb_chance_messages_prey: string[];
+ digest_messages_owner: string[];
+ digest_messages_prey: string[];
+ absorb_messages_owner: string[];
+ absorb_messages_prey: string[];
+ unabsorb_messages_owner: string[];
+ unabsorb_messages_prey: string[];
+ examine_messages: string[];
+ examine_messages_absorbed: string[];
+ trash_eater_in: string[];
+ trash_eater_out: string[];
+ displayed_message_flags: number;
+
+ // emote_list: string[];
+ emotes_digest: string[];
+ emotes_hold: string[];
+ emotes_holdabsorbed: string[];
+ emotes_absorb: string[];
+ emotes_heal: string[];
+ emotes_drain: string[];
+ emotes_steal: string[];
+ emotes_egg: string[];
+ emotes_shrink: string[];
+ emotes_grow: string[];
+ emotes_unabsorb: string[];
+
+ // Sounds
+ is_wet: BooleanLike;
+ wet_loop: BooleanLike;
+ fancy_vore: BooleanLike;
+ vore_sound: string;
+ release_sound: string;
+ sound_volume: number;
+ noise_freq: number;
+
+ // Visuals
+ affects_vore_sprites: BooleanLike;
+ count_absorbed_prey_for_sprite: BooleanLike;
+ absorbed_multiplier: number;
+ count_liquid_for_sprite: BooleanLike;
+ liquid_multiplier: number;
+ count_items_for_sprite: BooleanLike;
+ item_multiplier: number;
+ health_impacts_size: BooleanLike;
+ resist_triggers_animation: BooleanLike;
+ size_factor_for_sprite: number;
+ belly_sprite_to_affect: string;
+
+ // Visuals (Belly Fullscreens Preview and Coloring)
+ belly_fullscreen: string;
+ belly_fullscreen_color: string;
+ belly_fullscreen_color2: string;
+ belly_fullscreen_color3: string;
+ belly_fullscreen_color4: string;
+ belly_fullscreen_alpha: number;
+ colorization_enabled: BooleanLike;
+
+ // Visuals (Vore FX)
+ disable_hud: BooleanLike;
+
+ // Interactions
+ escapable: BooleanLike;
+
+ escapechance: number;
+ escapechance_absorbed: number;
+ escapetime: number;
+
+ transferchance: number;
+ transferlocation: string;
+
+ transferchance_secondary: number;
+ transferlocation_secondary: string;
+
+ absorbchance: number;
+ digestchance: number;
+ belchchance: number;
+
+ // Interactions (Auto-Transfer)
+ autotransferwait: number;
+ autotransferchance: number;
+ autotransferlocation: string;
+ autotransferextralocation: string[];
+ autotransfer_enabled: BooleanLike;
+ autotransferchance_secondary: number;
+ autotransferlocation_secondary: string;
+ autotransferextralocation_secondary: string[];
+ autotransfer_min_amount: number;
+ autotransfer_max_amount: number;
+ autotransfer_whitelist: string[];
+ autotransfer_blacklist: string[];
+ autotransfer_secondary_whitelist: string[];
+ autotransfer_secondary_blacklist: string[];
+ autotransfer_whitelist_items: string[];
+ autotransfer_blacklist_items: string[];
+ autotransfer_secondary_whitelist_items: string[];
+ autotransfer_secondary_blacklist_items: string[];
+
+ // Liquid Options
+ show_liquids: BooleanLike;
+ reagentbellymode: BooleanLike;
+ reagent_chosen: string;
+ reagent_name: string;
+ reagent_transfer_verb: string;
+ gen_time_display: string;
+ custom_max_volume: number;
+ vorefootsteps_sounds: BooleanLike;
+ reagent_mode_flag_list: string[];
+ liquid_overlay: BooleanLike;
+ max_liquid_level: number;
+ reagent_touches: BooleanLike;
+ mush_overlay: BooleanLike;
+ mush_color: string;
+ mush_alpha: number;
+ max_mush: number;
+ min_mush: number;
+ item_mush_val: number;
+ custom_reagentcolor: string;
+ custom_reagentalpha: number;
+ metabolism_overlay: BooleanLike;
+ metabolism_mush_ratio: number;
+ max_ingested: number;
+ custom_ingested_color: string;
+ custom_ingested_alpha: number;
+
+ // Liquid Messages
+ liquid_fullness1_messages: BooleanLike;
+ liquid_fullness2_messages: BooleanLike;
+ liquid_fullness3_messages: BooleanLike;
+ liquid_fullness4_messages: BooleanLike;
+ liquid_fullness5_messages: BooleanLike;
+
+ fullness1_messages: string[];
+ fullness2_messages: string[];
+ fullness3_messages: string[];
+ fullness4_messages: string[];
+ fullness5_messages: string[];
+};
+
+export type Soulcatcher = {
+ name: string;
+ inside_flavor: string;
+ capture_message: string;
+ transit_message: string;
+ release_message: string;
+ transfer_message: string;
+ delete_message: string;
+ linked_belly: string;
+ setting_flags: number;
+};
+
+type Formatter = (val: T) => string;
+
+export type SettingItem = {
+ label: string;
+ value: T;
+ formatter?: Formatter;
+};
+
+export type EmoteEntry = { label: string; messages?: string[] };
diff --git a/tgui/packages/tgui/interfaces/VorePanelImport/ImportElements/CharacterSelector.tsx b/tgui/packages/tgui/interfaces/VorePanelImport/ImportElements/CharacterSelector.tsx
new file mode 100644
index 00000000000..70a149a5436
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/VorePanelImport/ImportElements/CharacterSelector.tsx
@@ -0,0 +1,213 @@
+import { Fragment, useState } from 'react';
+import {
+ Box,
+ Button,
+ Icon,
+ Input,
+ LabeledList,
+ Section,
+ Stack,
+ Tooltip,
+} from 'tgui-core/components';
+import { createSearch } from 'tgui-core/string';
+import { getCurrentTimestamp } from '../../VorePanelExport/VorePanelExportTimestamp';
+import { CURRENT_VERSION, UNKNOWN_ORIGIN } from '../constants';
+import { importLengthToColor } from '../function';
+import type { DesiredData } from '../types';
+
+export const CharacterSelector = (props: {
+ characterData: DesiredData;
+ selectedCharacters: Set;
+ onCharacterData: React.Dispatch>;
+ onSelectedCharacters: React.Dispatch>>;
+ importLength: number;
+ selectedVersions: string[];
+ selectedOrigins: string[];
+}) => {
+ const {
+ characterData,
+ selectedCharacters,
+ onCharacterData,
+ onSelectedCharacters,
+ importLength,
+ selectedVersions,
+ selectedOrigins,
+ } = props;
+
+ const [searchText, setSearchText] = useState('');
+
+ const chracterSearch = createSearch(searchText, (name: string) => name);
+
+ const characterNames = Object.keys(characterData)
+ .filter(chracterSearch)
+ .sort((a: string, b: string) => a.localeCompare(b));
+
+ function toggleCharacter(name: string) {
+ onSelectedCharacters((prevSet) => {
+ const nextSet = new Set(prevSet);
+ if (nextSet.has(name)) {
+ nextSet.delete(name);
+ } else {
+ nextSet.add(name);
+ }
+ return nextSet;
+ });
+ }
+
+ function handleRename(target: string, newName: string) {
+ if (target === newName) return;
+ if (characterData[newName]) return;
+ if (!characterData[target]) return;
+
+ const nextCharacterData = { ...characterData };
+ nextCharacterData[newName] = nextCharacterData[target];
+ delete nextCharacterData[target];
+ onCharacterData(nextCharacterData);
+ onSelectedCharacters((prevSet) => {
+ const nextSet = new Set(prevSet);
+ if (nextSet.has(target)) {
+ nextSet.delete(target);
+ nextSet.add(newName);
+ }
+ return nextSet;
+ });
+ }
+
+ function handleMerge() {
+ const updatedData = Object.fromEntries(
+ Array.from(selectedCharacters).map((name) => {
+ const original = structuredClone(characterData[name]);
+ if (
+ typeof original.version === 'string' &&
+ Number.parseFloat(original.version) > 0.1
+ ) {
+ original.version = String(CURRENT_VERSION);
+ }
+ return [name, original];
+ }),
+ );
+
+ const blob = new Blob([JSON.stringify(updatedData)], {
+ type: 'application/json',
+ });
+
+ Byond.saveBlob(
+ blob,
+ Array.from(selectedCharacters).join('_') + getCurrentTimestamp(),
+ '.vrdb',
+ );
+ }
+
+ return (
+
+
+
+ Merge/Migrate
+
+ }
+ >
+
+ {characterNames.map((character) => (
+
+
+
+ toggleCharacter(character)}
+ >
+ {character}
+
+
+ {typeof characterData[character].version === 'string' &&
+ Number.parseFloat(characterData[character].version) <
+ CURRENT_VERSION && (
+
+
+
+
+
+ )}
+
+
+ handleRename(character, value)}
+ />
+
+
+
+ ))}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {importLength}
+
+
+
+ {selectedVersions.map((version, index) => (
+
+
+ {version}
+
+ {index < selectedVersions.length - 1 && ', '}
+
+ ))}
+
+
+ {selectedOrigins.map((origin, index) => (
+
+
+ {origin}
+
+ {index < selectedVersions.length - 1 && ', '}
+
+ ))}
+
+
+
+
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/VorePanelImport/ImportElements/FileImporter.tsx b/tgui/packages/tgui/interfaces/VorePanelImport/ImportElements/FileImporter.tsx
new file mode 100644
index 00000000000..1e3cb710fe2
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/VorePanelImport/ImportElements/FileImporter.tsx
@@ -0,0 +1,100 @@
+import { useState } from 'react';
+import { Box, Button, Dropdown, Section, Stack } from 'tgui-core/components';
+import { handleImportData } from '../function';
+import type { DesiredData } from '../types';
+
+export const FileImport = (props: {
+ characterData: DesiredData;
+ selectedCharacters: Set;
+ selectedCharacter: string;
+ onCharacterData: React.Dispatch>;
+ onSelectedCharacters: React.Dispatch>>;
+ onSelectedCharacter: React.Dispatch>;
+}) => {
+ const {
+ characterData,
+ selectedCharacters,
+ selectedCharacter,
+ onCharacterData,
+ onSelectedCharacters,
+ onSelectedCharacter,
+ } = props;
+ const [fileInputKey, setFileInputKey] = useState(0);
+
+ const ourCharacters = Object.keys(characterData);
+
+ function handleDeletion() {
+ const nextCharacterData = { ...characterData };
+ delete nextCharacterData[selectedCharacter];
+ onCharacterData(nextCharacterData);
+ onSelectedCharacter('');
+
+ const nextSelectedCharacters = new Set(selectedCharacters);
+ nextSelectedCharacters.delete(selectedCharacter);
+ onSelectedCharacters(nextSelectedCharacters);
+ }
+
+ function handleClearAll() {
+ onCharacterData({});
+ onSelectedCharacters(new Set());
+ onSelectedCharacter('');
+ }
+
+ return (
+
+
+
+ {
+ const imported = handleImportData(files);
+ onCharacterData({
+ ...characterData,
+ ...imported,
+ });
+ setFileInputKey((index) => index + 1);
+ }}
+ >
+ Import bellies
+
+
+
+
+
+
+
+
+
+
+
+
+ Loaded Characters:
+
+
+ {ourCharacters.length}
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/VorePanelImport/ImportElements/ImportDataSelector.tsx b/tgui/packages/tgui/interfaces/VorePanelImport/ImportElements/ImportDataSelector.tsx
new file mode 100644
index 00000000000..232a997e14d
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/VorePanelImport/ImportElements/ImportDataSelector.tsx
@@ -0,0 +1,128 @@
+import { useState } from 'react';
+import { Box, Button, Input, Section, Stack } from 'tgui-core/components';
+import { createSearch } from 'tgui-core/string';
+import { useBackend } from 'tgui/backend';
+import type { DesiredData } from '../types';
+
+export const ImportDataSelector = (props: {
+ characterData: DesiredData;
+ selectedCharacters: Set;
+ selectedBellies: Set;
+ onSelectedBellies: React.Dispatch>>;
+ activeTab: string;
+}) => {
+ const { act } = useBackend();
+ const {
+ characterData,
+ selectedCharacters,
+ selectedBellies,
+ onSelectedBellies,
+ activeTab,
+ } = props;
+
+ const [searchText, setSearchText] = useState('');
+
+ const filteredData = Object.fromEntries(
+ Array.from(selectedCharacters).map((name) => [name, characterData[name]]),
+ );
+
+ const bellySearch = createSearch(
+ searchText,
+ (belly: { name: string }) => belly.name,
+ );
+
+ const belliesToShow = Object.values(
+ filteredData[activeTab]?.bellies ?? [],
+ ).filter(bellySearch);
+
+ function toggleBelly(name: string | number | null) {
+ const stringValue = String(name);
+ onSelectedBellies((prevSet) => {
+ const nextSet = new Set(prevSet);
+ if (nextSet.has(stringValue)) {
+ nextSet.delete(stringValue);
+ } else {
+ nextSet.add(stringValue);
+ }
+ return nextSet;
+ });
+ }
+
+ function toggleAllBellies() {
+ const allBellyNames = Object.values(
+ filteredData[activeTab]?.bellies ?? {},
+ ).map((b) => String(b.name));
+ const allSelected = allBellyNames.every((name) =>
+ selectedBellies.has(name),
+ );
+
+ onSelectedBellies(allSelected ? new Set() : new Set(allBellyNames));
+ }
+
+ function handleImport() {
+ const bellies = filteredData[activeTab]?.bellies ?? {};
+ const selected = Object.values(bellies).filter((b) =>
+ selectedBellies.has(String(b.name)),
+ );
+ act('import_bellies', { data: selected });
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+ }
+ >
+
+
+
+
+
+
+
+
+ {belliesToShow.map((value) => (
+
+ toggleBelly(value.name)}
+ >
+ {value.name}
+
+
+ ))}
+
+
+
+
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/VorePanelImport/constants.ts b/tgui/packages/tgui/interfaces/VorePanelImport/constants.ts
new file mode 100644
index 00000000000..56753121cc6
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/VorePanelImport/constants.ts
@@ -0,0 +1,3 @@
+export const CURRENT_VERSION = 0.3;
+
+export const UNKNOWN_ORIGIN = 'unknown';
diff --git a/tgui/packages/tgui/interfaces/VorePanelImport/function.ts b/tgui/packages/tgui/interfaces/VorePanelImport/function.ts
new file mode 100644
index 00000000000..79d81186c9a
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/VorePanelImport/function.ts
@@ -0,0 +1,94 @@
+import { UNKNOWN_ORIGIN } from './constants';
+import type { DesiredData, ImportData } from './types';
+
+export function importLengthToColor(importLength: number): string {
+ if (importLength < 200000) {
+ return 'green';
+ }
+ if (importLength < 30000) {
+ return 'yellow';
+ }
+ return 'red';
+}
+
+export function handleImportData(importString: string | string[]): DesiredData {
+ const ourInput = Array.isArray(importString) ? importString[0] : importString;
+ let parsedData: ImportData | Record;
+ try {
+ parsedData = JSON.parse(ourInput);
+ if (Array.isArray(parsedData)) {
+ const ourBellies = {
+ unknown: {
+ bellies: Array.isArray(parsedData) ? parsedData : [],
+ soulcatcher: undefined,
+ version: '0.1',
+ repo: UNKNOWN_ORIGIN,
+ },
+ };
+ return ourBellies;
+ }
+
+ if (parsedData.bellies && parsedData.soulcatcher) {
+ const ourBellies = {
+ unknown: {
+ bellies: Array.isArray(parsedData.bellies) ? parsedData.bellies : [],
+ soulcatcher: isValidRecord(parsedData.soulcatcher)
+ ? parsedData.soulcatcher
+ : {},
+ version: '0.2',
+ repo: UNKNOWN_ORIGIN,
+ },
+ };
+ return ourBellies;
+ }
+
+ const ourBellies = Object.fromEntries(
+ Object.entries(parsedData).map(([name, ourData]) => {
+ if (isRecord(ourData)) {
+ return [
+ name,
+ {
+ bellies: Array.isArray(ourData.bellies) ? ourData.bellies : [],
+ soulcatcher: isValidRecord(ourData.soulcatcher)
+ ? ourData.soulcatcher
+ : {},
+ version: ourData.version,
+ repo: ourData.repo,
+ },
+ ];
+ } else {
+ return [
+ name,
+ {
+ bellies: {},
+ soulcatcher: {},
+ version: '0.3',
+ repo: UNKNOWN_ORIGIN,
+ },
+ ];
+ }
+ }),
+ );
+ return ourBellies;
+ } catch (err) {
+ console.error('Failed to parse JSON:', err);
+ }
+
+ return {};
+}
+function isRecord(value: unknown): value is Record {
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
+}
+
+function isValidRecord(
+ value: unknown,
+): value is Record {
+ return (
+ typeof value === 'object' &&
+ value !== null &&
+ !Array.isArray(value) &&
+ Object.values(value).every(
+ (v) => typeof v === 'string' || typeof v === 'number' || v === null,
+ )
+ );
+}
diff --git a/tgui/packages/tgui/interfaces/VorePanelImport/index.tsx b/tgui/packages/tgui/interfaces/VorePanelImport/index.tsx
new file mode 100644
index 00000000000..b6b06661764
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/VorePanelImport/index.tsx
@@ -0,0 +1,123 @@
+import { useEffect, useState } from 'react';
+import { Window } from 'tgui/layouts';
+import { Stack, Tabs } from 'tgui-core/components';
+import { CharacterSelector } from './ImportElements/CharacterSelector';
+import { FileImport } from './ImportElements/FileImporter';
+import { ImportDataSelector } from './ImportElements/ImportDataSelector';
+import type { DesiredData } from './types';
+
+export const VorePanelImport = () => {
+ const [characterData, setCharacterData] = useState({});
+ const [selectedCharacter, setSelectedCharacter] = useState('');
+ const [selectedCharacters, setSelectedCharacters] = useState>(
+ new Set(),
+ );
+ const [selectedBellies, setSelectedBellies] = useState>(
+ new Set(),
+ );
+ const [activeTab, setActiveTab] = useState('');
+ const [currentLength, setCurrentLength] = useState(0);
+ const [selectedVersions, setSelectedVersions] = useState([]);
+ const [selectedOrigins, setSelectedOrigins] = useState([]);
+
+ const filteredData = Object.fromEntries(
+ Array.from(selectedCharacters).map((name) => [name, characterData[name]]),
+ );
+
+ const ourCharacters = Object.keys(characterData);
+
+ useEffect(() => {
+ if (!activeTab && selectedCharacters.size) {
+ setActiveTab(Object.keys(filteredData)[0]);
+ }
+ if (activeTab && !selectedCharacters.has(activeTab)) {
+ setActiveTab('');
+ setSelectedBellies(new Set());
+ }
+ }, [ourCharacters]);
+
+ useEffect(() => {
+ const bellies = filteredData[activeTab]?.bellies ?? {};
+ const selected = Object.values(bellies).filter((b) =>
+ selectedBellies.has(String(b.name)),
+ );
+ setCurrentLength(JSON.stringify(selected).length);
+ }, [selectedBellies, filteredData, activeTab]);
+
+ useEffect(() => {
+ const allVersions = Array.from(
+ new Set(
+ Object.values(filteredData)
+ .map((dataEntry) => dataEntry.version)
+ .filter((v): v is string => typeof v === 'string'),
+ ),
+ );
+
+ const allOrigins = Array.from(
+ new Set(
+ Object.values(filteredData)
+ .map((dataEntry) => dataEntry.repo)
+ .filter((v): v is string => typeof v === 'string'),
+ ),
+ );
+ setSelectedVersions(allVersions);
+ setSelectedOrigins(allOrigins);
+ }, [filteredData]);
+
+ function handleTabChange(newTab: string) {
+ setActiveTab(newTab);
+ setSelectedBellies(new Set());
+ }
+
+ return (
+
+
+
+
+
+
+
+
+
+
+
+ {Object.keys(filteredData).map((entry) => (
+ handleTabChange(entry)}
+ key={entry}
+ >
+ {entry}
+
+ ))}
+
+
+
+
+
+
+
+
+ );
+};
diff --git a/tgui/packages/tgui/interfaces/VorePanelImport/types.ts b/tgui/packages/tgui/interfaces/VorePanelImport/types.ts
new file mode 100644
index 00000000000..370f48637dd
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/VorePanelImport/types.ts
@@ -0,0 +1,11 @@
+export type ImportData = DesiredData | Record;
+
+export type DesiredData = Record<
+ string,
+ {
+ bellies: Record[];
+ soulcatcher?: Record;
+ version?: string;
+ repo?: string;
+ }
+>;