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 += '
'; + 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 += '
'; + 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 += '
'; + 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 += `
'; + 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 += '
'; + 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 += '
    '; + 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 += '
'; + + 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 += '
'; + + 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 + + + + + + +
    + ); +}; 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; + } +>;