diff --git a/code/__DEFINES/ai/pets.dm b/code/__DEFINES/ai/pets.dm
index e41c9ac0c3f..c7383f56a00 100644
--- a/code/__DEFINES/ai/pets.dm
+++ b/code/__DEFINES/ai/pets.dm
@@ -51,3 +51,20 @@
/// key that holds items we arent interested in hoarding
#define BB_IGNORE_ITEMS "ignore_items"
+//virtual pet keys
+///the last PDA message we must relay
+#define BB_LAST_RECIEVED_MESSAGE "last_recieved_message"
+///our current virtual pet level
+#define BB_VIRTUAL_PET_LEVEL "virtual_pet_level"
+///the target we will play with
+#define BB_NEARBY_PLAYMATE "nearby_playmate"
+///cooldown till we search for playmates
+#define BB_NEXT_PLAYDATE "next_playdate"
+///our ability to trigger lights
+#define BB_LIGHTS_ABILITY "lights_ability"
+///our ability to capture images
+#define BB_PHOTO_ABILITY "photo_ability"
+///the name of our trick
+#define BB_TRICK_NAME "trick_name"
+///the sequence of our trick
+#define BB_TRICK_SEQUENCE "trick_sequence"
diff --git a/code/__DEFINES/dcs/signals/signals_action.dm b/code/__DEFINES/dcs/signals/signals_action.dm
index 6fbf5372acd..2226e34bccc 100644
--- a/code/__DEFINES/dcs/signals/signals_action.dm
+++ b/code/__DEFINES/dcs/signals/signals_action.dm
@@ -48,3 +48,6 @@
/// From /datum/action/cooldown/manual_heart/Activate(): ()
#define COMSIG_HEART_MANUAL_PULSE "heart_manual_pulse"
+
+/// From /datum/action/cooldown/mob_cooldown/capture_photo/Activate():
+#define COMSIG_ACTION_PHOTO_CAPTURED "action_photo_captured"
diff --git a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_main.dm b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_main.dm
index a027dc61adb..24524395f35 100644
--- a/code/__DEFINES/dcs/signals/signals_atom/signals_atom_main.dm
+++ b/code/__DEFINES/dcs/signals/signals_atom/signals_atom_main.dm
@@ -129,3 +129,8 @@
#define COMSIG_ATOM_GERM_UNEXPOSED "atom_germ_unexposed"
/// signal sent to puzzle pieces by activator
#define COMSIG_PUZZLE_COMPLETED "puzzle_completed"
+
+/// From /datum/compomnent/cleaner/clean()
+#define COMSIG_ATOM_PRE_CLEAN "atom_pre_clean"
+ ///cancel clean
+ #define COMSIG_ATOM_CANCEL_CLEAN (1<<0)
diff --git a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm
index 533ad2e1ae8..c8d36d27bcd 100644
--- a/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm
+++ b/code/__DEFINES/dcs/signals/signals_mob/signals_mob_living.dm
@@ -253,3 +253,11 @@
/// Sent to a mob grabbing another mob: (mob/living/grabbing)
#define COMSIG_LIVING_GRAB "living_grab"
// Return COMPONENT_CANCEL_ATTACK_CHAIN / COMPONENT_SKIP_ATTACK_CHAIN to stop the grab
+
+/// From /datum/element/basic_eating/try_eating()
+#define COMSIG_MOB_PRE_EAT "mob_pre_eat"
+ ///cancel eating attempt
+ #define COMSIG_MOB_CANCEL_EAT (1<<0)
+
+/// From /datum/element/basic_eating/finish_eating()
+#define COMSIG_MOB_ATE "mob_ate"
diff --git a/code/__DEFINES/dcs/signals/signals_object.dm b/code/__DEFINES/dcs/signals/signals_object.dm
index 442309289f0..3654b4cfce5 100644
--- a/code/__DEFINES/dcs/signals/signals_object.dm
+++ b/code/__DEFINES/dcs/signals/signals_object.dm
@@ -410,6 +410,11 @@
///from /datum/action/vehicle/sealed/headlights/vim/Trigger(): (headlights_on)
#define COMSIG_VIM_HEADLIGHTS_TOGGLED "vim_headlights_toggled"
+///from /datum/computer_file/program/messenger/proc/receive_message
+#define COMSIG_COMPUTER_RECIEVED_MESSAGE "computer_recieved_message"
+///from /datum/computer_file/program/virtual_pet/proc/handle_level_up
+#define COMSIG_VIRTUAL_PET_LEVEL_UP "virtual_pet_level_up"
+
// /obj/vehicle/sealed/mecha signals
/// sent if you attach equipment to mecha
diff --git a/code/datums/components/cleaner.dm b/code/datums/components/cleaner.dm
index 242ad72071c..49f200b4b92 100644
--- a/code/datums/components/cleaner.dm
+++ b/code/datums/components/cleaner.dm
@@ -89,9 +89,8 @@
*/
/datum/component/cleaner/proc/clean(datum/source, atom/target, mob/living/user, clean_target = TRUE)
//make sure we don't attempt to clean something while it's already being cleaned
- if(HAS_TRAIT(target, TRAIT_CURRENTLY_CLEANING))
+ if(HAS_TRAIT(target, TRAIT_CURRENTLY_CLEANING) || (SEND_SIGNAL(target, COMSIG_ATOM_PRE_CLEAN, user) & COMSIG_ATOM_CANCEL_CLEAN))
return
-
//add the trait and overlay
ADD_TRAIT(target, TRAIT_CURRENTLY_CLEANING, REF(src))
// We need to update our planes on overlay changes
diff --git a/code/datums/elements/basic_eating.dm b/code/datums/elements/basic_eating.dm
index 297e77fa060..2a7a4b46598 100644
--- a/code/datums/elements/basic_eating.dm
+++ b/code/datums/elements/basic_eating.dm
@@ -54,6 +54,8 @@
/datum/element/basic_eating/proc/try_eating(mob/living/eater, atom/target)
if(!is_type_in_list(target, food_types))
return FALSE
+ if(SEND_SIGNAL(eater, COMSIG_MOB_PRE_EAT, target) & COMSIG_MOB_CANCEL_EAT)
+ return FALSE
var/eat_verb
if(drinking)
eat_verb = pick("slurp","sip","guzzle","drink","quaff","suck")
@@ -79,6 +81,7 @@
return TRUE
/datum/element/basic_eating/proc/finish_eating(mob/living/eater, atom/target)
+ SEND_SIGNAL(eater, COMSIG_MOB_ATE)
if(drinking)
playsound(eater.loc,'sound/items/drink.ogg', rand(10,50), TRUE)
else
diff --git a/code/datums/elements/cleaning.dm b/code/datums/elements/cleaning.dm
index 3f39d00eb6e..6db1c9fb580 100644
--- a/code/datums/elements/cleaning.dm
+++ b/code/datums/elements/cleaning.dm
@@ -1,32 +1,36 @@
+/datum/element/cleaning
+
/datum/element/cleaning/Attach(datum/target)
. = ..()
if(!ismovable(target))
return ELEMENT_INCOMPATIBLE
- RegisterSignal(target, COMSIG_MOVABLE_MOVED, PROC_REF(Clean))
+ RegisterSignal(target, COMSIG_MOVABLE_MOVED, PROC_REF(clean))
/datum/element/cleaning/Detach(datum/target)
. = ..()
UnregisterSignal(target, COMSIG_MOVABLE_MOVED)
-/datum/element/cleaning/proc/Clean(datum/source)
+/datum/element/cleaning/proc/clean(datum/source)
SIGNAL_HANDLER
- var/atom/movable/AM = source
- var/turf/tile = AM.loc
+ var/atom/movable/atom_movable = source
+ var/turf/tile = atom_movable.loc
if(!isturf(tile))
return
tile.wash(CLEAN_SCRUB)
- for(var/A in tile)
+ for(var/atom/cleaned as anything in tile)
// Clean small items that are lying on the ground
- if(isitem(A))
- var/obj/item/I = A
- if(I.w_class <= WEIGHT_CLASS_SMALL && !ismob(I.loc))
- I.wash(CLEAN_SCRUB)
+ if(isitem(cleaned))
+ var/obj/item/cleaned_item = cleaned
+ if(cleaned_item.w_class <= WEIGHT_CLASS_SMALL)
+ cleaned_item.wash(CLEAN_SCRUB)
+ continue
// Clean humans that are lying down
- else if(ishuman(A))
- var/mob/living/carbon/human/cleaned_human = A
- if(cleaned_human.body_position == LYING_DOWN)
- cleaned_human.wash(CLEAN_SCRUB)
- cleaned_human.regenerate_icons()
- to_chat(cleaned_human, span_danger("[AM] cleans your face!"))
+ if(!ishuman(cleaned))
+ continue
+ var/mob/living/carbon/human/cleaned_human = cleaned
+ if(cleaned_human.body_position == LYING_DOWN)
+ cleaned_human.wash(CLEAN_SCRUB)
+ cleaned_human.regenerate_icons()
+ to_chat(cleaned_human, span_danger("[atom_movable] cleans your face!"))
diff --git a/code/datums/emotes.dm b/code/datums/emotes.dm
index cc9d5820c91..2a6650e9731 100644
--- a/code/datums/emotes.dm
+++ b/code/datums/emotes.dm
@@ -60,12 +60,6 @@
var/can_message_change = FALSE
/// How long is the cooldown on the audio of the emote, if it has one?
var/audio_cooldown = 2 SECONDS
- //NOVA EDIT ADDITION BEGIN - EMOTES
- var/sound_volume = 25 //Emote volume
- var/list/allowed_species
- /// Are silicons explicitely allowed to use this emote?
- var/silicon_allowed = FALSE
- //NOVA EDIT ADDITION END
/datum/emote/New()
switch(mob_type_allowed_typecache)
@@ -299,15 +293,12 @@
return FALSE
//NOVA EDIT BEGIN
- if(allowed_species)
- var/check = FALSE
- if(silicon_allowed && issilicon(user))
- check = TRUE
- if(ishuman(user))
- var/mob/living/carbon/human/sender = user
- if(sender.dna.species.type in allowed_species)
- check = TRUE
- return check
+ if(allowed_species && ishuman(user))
+ var/mob/living/carbon/human/sender = user
+ if(sender.dna.species.type in allowed_species)
+ return TRUE
+ else
+ return FALSE
//NOVA EDIT END
return TRUE
diff --git a/code/datums/greyscale/config_types/greyscale_configs/greyscale_mobs.dm b/code/datums/greyscale/config_types/greyscale_configs/greyscale_mobs.dm
index 250eba9a0d5..87799dedda5 100644
--- a/code/datums/greyscale/config_types/greyscale_configs/greyscale_mobs.dm
+++ b/code/datums/greyscale/config_types/greyscale_configs/greyscale_mobs.dm
@@ -44,4 +44,3 @@
name = "Gutlunch"
icon_file = 'icons/mob/simple/lavaland/lavaland_monsters.dmi'
json_config = 'code/datums/greyscale/json_configs/gutlunch.json'
-
diff --git a/code/game/objects/items/food/sweets.dm b/code/game/objects/items/food/sweets.dm
index 5c638077d16..d757261ac01 100644
--- a/code/game/objects/items/food/sweets.dm
+++ b/code/game/objects/items/food/sweets.dm
@@ -79,6 +79,15 @@
w_class = WEIGHT_CLASS_TINY
crafting_complexity = FOOD_COMPLEXITY_1
+/obj/item/food/virtual_chocolate
+ name = "virtual chocolate bar"
+ desc = "Digital food only gives off the sensation of eating... without any of the nutritional benefits."
+ icon_state = "virtual_chocolate"
+ tastes = list("nothing" = 1)
+ foodtypes = NONE
+ w_class = WEIGHT_CLASS_TINY
+
+
/obj/item/food/chococoin
name = "chocolate coin"
desc = "A completely edible but non-flippable festive coin."
diff --git a/code/modules/mob/living/basic/pets/orbie/orbie.dm b/code/modules/mob/living/basic/pets/orbie/orbie.dm
new file mode 100644
index 00000000000..2c9fb3d815c
--- /dev/null
+++ b/code/modules/mob/living/basic/pets/orbie/orbie.dm
@@ -0,0 +1,112 @@
+#define ORBIE_MAXIMUM_HEALTH 300
+
+/mob/living/basic/orbie
+ name = "Orbie"
+ desc = "An orb shaped hologram."
+ icon = 'icons/mob/simple/pets.dmi'
+ icon_state = "orbie"
+ icon_living = "orbie"
+ speed = 0
+ maxHealth = 100
+ light_on = FALSE
+ light_system = OVERLAY_LIGHT
+ light_range = 6
+ light_color = "#64bee1"
+ health = 100
+ habitable_atmos = list("min_oxy" = 0, "max_oxy" = 0, "min_plas" = 0, "max_plas" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0)
+ unsuitable_atmos_damage = 0
+ can_buckle_to = FALSE
+ density = FALSE
+ pass_flags = PASSMOB
+ move_force = 0
+ move_resist = 0
+ pull_force = 0
+ minimum_survivable_temperature = TCMB
+ maximum_survivable_temperature = INFINITY
+ death_message = "fades out of existence!"
+ ai_controller = /datum/ai_controller/basic_controller/orbie
+ ///are we happy or not?
+ var/happy_state = FALSE
+ ///overlay for our neutral eyes
+ var/static/mutable_appearance/eyes_overlay = mutable_appearance('icons/mob/simple/pets.dmi', "orbie_eye_overlay")
+ ///overlay for when our eyes are emitting light
+ var/static/mutable_appearance/orbie_light_overlay = mutable_appearance('icons/mob/simple/pets.dmi', "orbie_light_overlay")
+ ///overlay for the flame propellar
+ var/static/mutable_appearance/flame_overlay = mutable_appearance('icons/mob/simple/pets.dmi', "orbie_flame_overlay")
+ ///overlay for our happy eyes
+ var/static/mutable_appearance/happy_eyes_overlay = mutable_appearance('icons/mob/simple/pets.dmi', "orbie_happy_eye_overlay")
+ ///commands we can give orbie
+ var/list/pet_commands = list(
+ /datum/pet_command/idle,
+ /datum/pet_command/free,
+ /datum/pet_command/untargeted_ability/pet_lights,
+ /datum/pet_command/point_targeting/use_ability/take_photo,
+ /datum/pet_command/follow/orbie,
+ /datum/pet_command/perform_trick_sequence,
+ )
+
+/mob/living/basic/orbie/Initialize(mapload)
+ . = ..()
+ var/static/list/food_types = list(/obj/item/food/virtual_chocolate)
+ AddComponent(/datum/component/obeys_commands, pet_commands)
+ AddElement(/datum/element/basic_eating, food_types = food_types)
+ RegisterSignal(src, COMSIG_ATOM_CAN_BE_PULLED, PROC_REF(on_pulled))
+ RegisterSignal(src, COMSIG_VIRTUAL_PET_LEVEL_UP, PROC_REF(on_level_up))
+ RegisterSignal(src, COMSIG_MOB_CLICKON, PROC_REF(on_click))
+ RegisterSignal(src, COMSIG_ATOM_UPDATE_LIGHT_ON, PROC_REF(on_lights))
+ ai_controller.set_blackboard_key(BB_BASIC_FOODS, typecacheof(food_types))
+ update_appearance()
+
+/mob/living/basic/orbie/proc/on_click(mob/living/basic/source, atom/target, params)
+ SIGNAL_HANDLER
+
+ if(!CanReach(target))
+ return
+
+ if(src == target || happy_state || !istype(target))
+ return
+
+ toggle_happy_state()
+ addtimer(CALLBACK(src, PROC_REF(toggle_happy_state)), 30 SECONDS)
+
+/mob/living/basic/orbie/proc/on_lights(datum/source)
+ SIGNAL_HANDLER
+
+ update_appearance()
+
+/mob/living/basic/orbie/proc/toggle_happy_state()
+ happy_state = !happy_state
+ update_appearance()
+
+/mob/living/basic/orbie/proc/on_pulled(datum/source) //i need move resist at 0, but i also dont want him to be pulled
+ SIGNAL_HANDLER
+
+ return COMSIG_ATOM_CANT_PULL
+
+/mob/living/basic/orbie/proc/on_level_up(datum/source, new_level)
+ SIGNAL_HANDLER
+
+ if(maxHealth >= ORBIE_MAXIMUM_HEALTH)
+ UnregisterSignal(src, COMSIG_VIRTUAL_PET_LEVEL_UP)
+ return
+
+ maxHealth += 100
+ heal_overall_damage(maxHealth - health)
+
+
+/mob/living/basic/orbie/update_overlays()
+ . = ..()
+ if(stat == DEAD)
+ return
+ . += flame_overlay
+ if(happy_state)
+ . += happy_eyes_overlay
+ else if(light_on)
+ . += orbie_light_overlay
+ else
+ . += eyes_overlay
+
+/mob/living/basic/orbie/gib()
+ death(TRUE)
+
+#undef ORBIE_MAXIMUM_HEALTH
diff --git a/code/modules/mob/living/basic/pets/orbie/orbie_abilities.dm b/code/modules/mob/living/basic/pets/orbie/orbie_abilities.dm
new file mode 100644
index 00000000000..fb9994a9321
--- /dev/null
+++ b/code/modules/mob/living/basic/pets/orbie/orbie_abilities.dm
@@ -0,0 +1,46 @@
+/datum/action/cooldown/mob_cooldown/lights
+ name = "Toggle Lights"
+ button_icon = 'icons/mob/simple/pets.dmi'
+ button_icon_state = "orbie_light_action"
+ background_icon_state = "bg_default"
+ overlay_icon_state = "bg_default_border"
+ click_to_activate = FALSE
+
+/datum/action/cooldown/mob_cooldown/lights/Activate()
+ owner.set_light_on(!owner.light_on)
+ return TRUE
+
+
+/datum/action/cooldown/mob_cooldown/capture_photo
+ name = "Camera"
+ button_icon = 'icons/mob/simple/pets.dmi'
+ button_icon_state = "orbie_light_action"
+ background_icon_state = "bg_default"
+ overlay_icon_state = "bg_default_border"
+ cooldown_time = 30 SECONDS
+ ///camera we use to take photos
+ var/obj/item/camera/ability_camera
+
+/datum/action/cooldown/mob_cooldown/capture_photo/Grant(mob/grant_to)
+ . = ..()
+ if(isnull(owner))
+ return
+ ability_camera = new(owner)
+ ability_camera.print_picture_on_snap = FALSE
+ RegisterSignal(ability_camera, COMSIG_PREQDELETED, PROC_REF(on_camera_delete))
+
+/datum/action/cooldown/mob_cooldown/capture_photo/Activate(atom/target)
+ if(isnull(ability_camera))
+ return FALSE
+ ability_camera.captureimage(target, owner)
+ StartCooldown()
+ return TRUE
+
+/datum/action/cooldown/mob_cooldown/capture_photo/proc/on_camera_delete(datum/source)
+ SIGNAL_HANDLER
+ UnregisterSignal(ability_camera, COMSIG_PREQDELETED)
+ ability_camera = null
+
+/datum/action/cooldown/mob_cooldown/capture_photo/Destroy()
+ QDEL_NULL(ability_camera)
+ return ..()
diff --git a/code/modules/mob/living/basic/pets/orbie/orbie_ai.dm b/code/modules/mob/living/basic/pets/orbie/orbie_ai.dm
new file mode 100644
index 00000000000..854a0209464
--- /dev/null
+++ b/code/modules/mob/living/basic/pets/orbie/orbie_ai.dm
@@ -0,0 +1,169 @@
+#define PET_PLAYTIME_COOLDOWN (2 MINUTES)
+#define MESSAGE_EXPIRY_TIME (30 SECONDS)
+
+/datum/ai_controller/basic_controller/orbie
+ blackboard = list(
+ BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic/allow_items,
+ BB_PET_TARGETING_STRATEGY = /datum/targeting_strategy/basic/not_friends,
+ BB_TRICK_NAME = "Trick",
+ )
+
+ ai_movement = /datum/ai_movement/basic_avoidance
+ idle_behavior = /datum/idle_behavior/idle_random_walk
+ planning_subtrees = list(
+ /datum/ai_planning_subtree/find_food,
+ /datum/ai_planning_subtree/find_playmates,
+ /datum/ai_planning_subtree/basic_melee_attack_subtree,
+ /datum/ai_planning_subtree/relay_pda_message,
+ /datum/ai_planning_subtree/pet_planning,
+ )
+
+/datum/ai_controller/basic_controller/orbie/TryPossessPawn(atom/new_pawn)
+ . = ..()
+ if(. & AI_CONTROLLER_INCOMPATIBLE)
+ return
+ RegisterSignal(new_pawn, COMSIG_AI_BLACKBOARD_KEY_SET(BB_LAST_RECIEVED_MESSAGE), PROC_REF(on_set_message))
+
+/datum/ai_controller/basic_controller/orbie/proc/on_set_message(datum/source)
+ SIGNAL_HANDLER
+
+ addtimer(CALLBACK(src, PROC_REF(clear_blackboard_key), BB_LAST_RECIEVED_MESSAGE), MESSAGE_EXPIRY_TIME)
+
+///ai behavior that lets us search for other orbies to play with
+/datum/ai_planning_subtree/find_playmates
+
+/datum/ai_planning_subtree/find_playmates/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ if(controller.blackboard[BB_NEXT_PLAYDATE] > world.time)
+ return
+ if(controller.blackboard_key_exists(BB_NEARBY_PLAYMATE))
+ controller.queue_behavior(/datum/ai_behavior/interact_with_playmate, BB_NEARBY_PLAYMATE)
+ return SUBTREE_RETURN_FINISH_PLANNING
+
+ controller.queue_behavior(/datum/ai_behavior/find_and_set/find_playmate, BB_NEARBY_PLAYMATE, /mob/living/basic/orbie)
+
+/datum/ai_behavior/find_and_set/find_playmate
+
+/datum/ai_behavior/find_and_set/find_playmate/search_tactic(datum/ai_controller/controller, locate_path, search_range)
+ for(var/mob/living/basic/orbie/playmate in oview(search_range, controller.pawn))
+ if(playmate == controller.pawn || playmate.stat == DEAD || isnull(playmate.ai_controller))
+ continue
+ if(playmate.ai_controller.blackboard[BB_NEARBY_PLAYMATE] || playmate.ai_controller.blackboard[BB_NEXT_PLAYDATE] > world.time) //they already have a playmate...
+ continue
+ playmate.ai_controller.set_blackboard_key(BB_NEARBY_PLAYMATE, controller.pawn)
+ return playmate
+ return null
+
+
+/datum/ai_behavior/interact_with_playmate
+ behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
+
+/datum/ai_behavior/interact_with_playmate/setup(datum/ai_controller/controller, target_key)
+ . = ..()
+ var/turf/target = controller.blackboard[target_key]
+ if(isnull(target))
+ return FALSE
+ set_movement_target(controller, target)
+
+/datum/ai_behavior/interact_with_playmate/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
+ . = ..()
+ var/mob/living/basic/living_pawn = controller.pawn
+ var/atom/target = controller.blackboard[target_key]
+
+ if(QDELETED(target))
+ finish_action(controller, FALSE, target_key)
+ return
+
+ living_pawn.manual_emote("plays with [target]!")
+ living_pawn.spin(spintime = 4, speed = 1)
+ living_pawn.ClickOn(target)
+ finish_action(controller, TRUE, target_key)
+
+/datum/ai_behavior/interact_with_playmate/finish_action(datum/ai_controller/controller, success, target_key)
+ . = ..()
+ controller.clear_blackboard_key(target_key)
+ controller.set_blackboard_key(BB_NEXT_PLAYDATE, world.time + PET_PLAYTIME_COOLDOWN)
+
+/datum/ai_planning_subtree/relay_pda_message
+
+/datum/ai_planning_subtree/relay_pda_message/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
+ if(controller.blackboard[BB_VIRTUAL_PET_LEVEL] < 2 || isnull(controller.blackboard[BB_LAST_RECIEVED_MESSAGE]))
+ return
+
+ controller.queue_behavior(/datum/ai_behavior/relay_pda_message, BB_LAST_RECIEVED_MESSAGE)
+
+/datum/ai_behavior/relay_pda_message/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
+ . = ..()
+ var/mob/living/basic/living_pawn = controller.pawn
+ var/text_to_say = controller.blackboard[target_key]
+ if(isnull(text_to_say))
+ finish_action(controller, FALSE, target_key)
+ return
+
+ living_pawn.say(text_to_say, forced = "AI controller")
+ living_pawn.spin(spintime = 4, speed = 1)
+ finish_action(controller, TRUE, target_key)
+
+/datum/ai_behavior/relay_pda_message/finish_action(datum/ai_controller/controller, success, target_key)
+ . = ..()
+ controller.clear_blackboard_key(target_key)
+
+/datum/pet_command/follow/orbie
+ follow_behavior = /datum/ai_behavior/pet_follow_friend/orbie
+
+/datum/ai_behavior/pet_follow_friend/orbie
+ behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
+
+///command to make our pet turn its lights on, we need to be level 2 to activate this ability
+/datum/pet_command/untargeted_ability/pet_lights
+ command_name = "Lights"
+ command_desc = "Toggle your pet's lights!"
+ radial_icon = 'icons/mob/simple/pets.dmi'
+ radial_icon_state = "orbie_lights_action"
+ speech_commands = list("lights", "light", "toggle")
+ ability_key = BB_LIGHTS_ABILITY
+
+/datum/pet_command/untargeted_ability/pet_lights/execute_action(datum/ai_controller/controller)
+ if(controller.blackboard[BB_VIRTUAL_PET_LEVEL] < 2)
+ controller.clear_blackboard_key(BB_ACTIVE_PET_COMMAND)
+ return SUBTREE_RETURN_FINISH_PLANNING
+ return ..()
+
+/datum/pet_command/point_targeting/use_ability/take_photo
+ command_name = "Photo"
+ command_desc = "Make your pet take a photo!"
+ radial_icon = 'icons/mob/simple/pets.dmi'
+ radial_icon_state = "orbie_lights_action"
+ speech_commands = list("photo", "picture", "image")
+ command_feedback = "Readys camera mode"
+ pet_ability_key = BB_PHOTO_ABILITY
+ targeting_strategy_key = BB_TARGETING_STRATEGY
+
+/datum/pet_command/point_targeting/use_ability/take_photo/execute_action(datum/ai_controller/controller)
+ if(controller.blackboard[BB_VIRTUAL_PET_LEVEL] < 3)
+ controller.clear_blackboard_key(BB_ACTIVE_PET_COMMAND)
+ return SUBTREE_RETURN_FINISH_PLANNING
+ return ..()
+
+/datum/pet_command/perform_trick_sequence
+ command_name = "Trick Sequence"
+ command_desc = "A trick sequence programmable through your PDA!"
+
+/datum/pet_command/perform_trick_sequence/find_command_in_text(spoken_text, check_verbosity = FALSE)
+ var/mob/living/living_pawn = weak_parent.resolve()
+ if(isnull(living_pawn?.ai_controller))
+ return FALSE
+ var/text_command = living_pawn.ai_controller.blackboard[BB_TRICK_NAME]
+ if(isnull(text_command))
+ return FALSE
+ return findtext(spoken_text, text_command)
+
+/datum/pet_command/perform_trick_sequence/execute_action(datum/ai_controller/controller)
+ var/mob/living/living_pawn = controller.pawn
+ var/list/trick_sequence = controller.blackboard[BB_TRICK_SEQUENCE]
+ for(var/index in 1 to length(trick_sequence))
+ addtimer(CALLBACK(living_pawn, TYPE_PROC_REF(/mob, emote), trick_sequence[index], index * 0.5 SECONDS))
+ controller.clear_blackboard_key(BB_ACTIVE_PET_COMMAND)
+ return SUBTREE_RETURN_FINISH_PLANNING
+
+#undef PET_PLAYTIME_COOLDOWN
+#undef MESSAGE_EXPIRY_TIME
diff --git a/code/modules/mob/living/emote.dm b/code/modules/mob/living/emote.dm
index 7b989ff494c..9adde080bf7 100644
--- a/code/modules/mob/living/emote.dm
+++ b/code/modules/mob/living/emote.dm
@@ -730,16 +730,14 @@
/datum/emote/living/custom/replace_pronoun(mob/user, message)
return message
-//NOVA EDIT REMOVAL BEGIN - SYNTH EMOTES, NOW HANDLED IN SYNTH_EMOTES.DM
-/* /datum/emote/living/beep
+/datum/emote/living/beep
key = "beep"
key_third_person = "beeps"
message = "beeps."
message_param = "beeps at %t."
sound = 'sound/machines/twobeep.ogg'
- mob_type_allowed_typecache = list(/mob/living/brain, /mob/living/silicon)
- emote_type = EMOTE_AUDIBLE */
-//NOVA EDIT REMOVAL END
+ mob_type_allowed_typecache = list(/mob/living/brain, /mob/living/silicon, /mob/living/basic/orbie)
+ emote_type = EMOTE_AUDIBLE
/datum/emote/living/inhale
key = "inhale"
diff --git a/code/modules/mob/living/silicon/robot/robot.dm b/code/modules/mob/living/silicon/robot/robot.dm
index 1d2385984ad..39306e63fe4 100644
--- a/code/modules/mob/living/silicon/robot/robot.dm
+++ b/code/modules/mob/living/silicon/robot/robot.dm
@@ -11,8 +11,8 @@
post_tipped_callback = CALLBACK(src, PROC_REF(after_tip_over)), \
post_untipped_callback = CALLBACK(src, PROC_REF(after_righted)), \
roleplay_friendly = TRUE, \
- roleplay_emotes = list(/datum/emote/living/human/buzz, /datum/emote/living/human/buzz2, /datum/emote/living/human/beep, /datum/emote/living/human/beep2), \
- roleplay_callback = CALLBACK(src, PROC_REF(untip_roleplay))) // NOVA EDIT CHANGE
+ roleplay_emotes = list(/datum/emote/living/human/buzz, /datum/emote/living/human/buzz2, /datum/emote/living/beep, /datum/emote/living/human/beep2), /* NOVA EDIT CHANGE - ORIGINAL: roleplay_emotes = list(/datum/emote/silicon/buzz, /datum/emote/silicon/buzz2, /datum/emote/living/beep), */ \
+ roleplay_callback = CALLBACK(src, PROC_REF(untip_roleplay)))
set_wires(new /datum/wires/robot(src))
AddElement(/datum/element/empprotection, EMP_PROTECT_WIRES)
diff --git a/code/modules/modular_computers/file_system/programs/messenger/messenger_program.dm b/code/modules/modular_computers/file_system/programs/messenger/messenger_program.dm
index 1ad20e698ad..c4626164256 100644
--- a/code/modules/modular_computers/file_system/programs/messenger/messenger_program.dm
+++ b/code/modules/modular_computers/file_system/programs/messenger/messenger_program.dm
@@ -718,6 +718,8 @@
var/photo_message = signal.data["photo"] ? " (Photo Attached)" : ""
to_chat(messaged_mob, span_infoplain("[icon2html(computer, messaged_mob)] PDA message from [sender_title], \"[inbound_message]\"[photo_message] [reply]"))
+ SEND_SIGNAL(computer, COMSIG_COMPUTER_RECIEVED_MESSAGE, sender_title, inbound_message, photo_message)
+
if (alert_able && (!alert_silenced || is_rigged))
computer.ring(ringtone)
diff --git a/code/modules/modular_computers/file_system/programs/virtual_pet.dm b/code/modules/modular_computers/file_system/programs/virtual_pet.dm
new file mode 100644
index 00000000000..1d3196789ca
--- /dev/null
+++ b/code/modules/modular_computers/file_system/programs/virtual_pet.dm
@@ -0,0 +1,568 @@
+GLOBAL_LIST_EMPTY(global_pet_updates)
+GLOBAL_LIST_EMPTY(virtual_pets_list)
+
+#define MAX_UPDATE_LENGTH 50
+#define PET_MAX_LEVEL 3
+#define PET_MAX_STEPS_RECORD 50000
+#define PET_EAT_BONUS 500
+#define PET_CLEAN_BONUS 250
+#define PET_PLAYMATE_BONUS 500
+#define PET_STATE_HUNGRY "hungry"
+#define PET_STATE_ASLEEP "asleep"
+#define PET_STATE_HAPPY "happy"
+#define PET_STATE_NEUTRAL "neutral"
+
+/datum/computer_file/program/virtual_pet
+ filename = "virtualpet"
+ filedesc = "Virtual Pet"
+ downloader_category = PROGRAM_CATEGORY_GAMES
+ extended_desc = "Download your very own Orbie today!"
+ program_flags = PROGRAM_ON_NTNET_STORE
+ size = 3
+ tgui_id = "NtosVirtualPet"
+ program_icon = "paw"
+ can_run_on_flags = PROGRAM_PDA
+ detomatix_resistance = DETOMATIX_RESIST_MALUS
+ ///how many steps have we walked
+ var/steps_counter = 0
+ ///the pet hologram
+ var/mob/living/pet
+ ///the type of our pet
+ var/pet_type = /mob/living/basic/orbie
+ ///our current happiness
+ var/happiness = 0
+ ///our max happiness
+ var/max_happiness = 1750
+ ///our current level
+ var/level = 1
+ ///required exp to get to next level
+ var/to_next_level = 1000
+ ///how much exp we currently have
+ var/current_level_progress = 0
+ ///our current hunger
+ var/hunger = 0
+ ///maximum hunger threshold
+ var/max_hunger = 500
+ ///pet icon for each state
+ var/static/list/pet_state_icons = list(
+ PET_STATE_HUNGRY = list("icon" = 'icons/ui_icons/virtualpet/pet_state.dmi', "icon_state" = "pet_hungry"),
+ PET_STATE_HAPPY = list("icon" = 'icons/ui_icons/virtualpet/pet_state.dmi', "icon_state" = "pet_happy"),
+ PET_STATE_ASLEEP = list("icon" = 'icons/ui_icons/virtualpet/pet_state.dmi', "icon_state" = "pet_asleep"),
+ PET_STATE_NEUTRAL = list("icon" = 'icons/ui_icons/virtualpet/pet_state.dmi', "icon_state" = "pet_neutral"),
+ )
+ ///hat options and what level they will be unlocked at
+ var/static/list/hat_selections = list(
+ /obj/item/clothing/head/hats/tophat = 1,
+ /obj/item/clothing/head/fedora = 1,
+ /obj/item/clothing/head/hats/bowler = 2,
+ /obj/item/clothing/head/hats/warden/police = 2,
+ /obj/item/clothing/head/hats/warden/red = 3,
+ /obj/item/clothing/head/hats/caphat = 3,
+ )
+ ///hologram hat we have selected for our pet
+ var/list/selected_hat = list()
+ ///area we have picked as dropoff location for petfeed
+ var/area/selected_area
+ ///manage hat offsets for when we turn directions
+ var/static/list/hat_offsets = list(
+ "west" = list(0,1),
+ "east" = list(0,1),
+ "north" = list(1,1),
+ "south" = list(0,1),
+ )
+ ///possible colors our pet can have
+ var/static/list/possible_colors= list(
+ "white" = null, //default color state
+ "light blue" = "#c3ecf3",
+ "light green" = "#b1ffe8",
+ )
+ ///areas we wont drop the chocolate in
+ var/static/list/restricted_areas = typecacheof(list(
+ /area/station/security,
+ /area/station/command,
+ /area/station/ai_monitored,
+ /area/station/maintenance,
+ /area/station/solars,
+ ))
+ ///our profile picture
+ var/icon/profile_picture
+ ///cooldown till we can reroll the pet feed dropzone
+ COOLDOWN_DECLARE(area_reroll)
+ ///cooldown till our pet gains happiness again from being cleaned
+ COOLDOWN_DECLARE(on_clean_cooldown)
+ ///cooldown till we can release/recall our pet
+ COOLDOWN_DECLARE(summon_cooldown)
+ ///cooldown till we can alter our pet's appearance again
+ COOLDOWN_DECLARE(alter_appearance_cooldown)
+
+/datum/computer_file/program/virtual_pet/on_install()
+ . = ..()
+ profile_picture = getFlatIcon(image(icon = 'icons/ui_icons/virtualpet/pet_state.dmi', icon_state = "pet_preview"))
+ GLOB.virtual_pets_list += src
+ pet = new pet_type(computer)
+ pet.forceMove(computer)
+ pet.AddComponent(/datum/component/leash, computer, 9, force_teleport_out_effect = /obj/effect/temp_visual/guardian/phase/out)
+ RegisterSignal(pet, COMSIG_QDELETING, PROC_REF(remove_pet))
+ RegisterSignal(pet, COMSIG_ATOM_UPDATE_OVERLAYS, PROC_REF(on_overlays_updated)) //hologramic hat management
+ RegisterSignal(pet, COMSIG_ATOM_DIR_CHANGE, PROC_REF(on_change_dir))
+ RegisterSignal(pet, COMSIG_MOVABLE_MOVED, PROC_REF(after_pet_move))
+ RegisterSignal(pet, COMSIG_MOB_ATE, PROC_REF(after_pet_eat)) // WE ATEEE
+ RegisterSignal(pet, COMSIG_ATOM_PRE_CLEAN, PROC_REF(pet_pre_clean))
+ RegisterSignal(pet, COMSIG_LIVING_DEATH, PROC_REF(on_death))
+ RegisterSignal(pet, COMSIG_COMPONENT_CLEAN_ACT, PROC_REF(post_cleaned))
+ RegisterSignal(pet, COMSIG_AI_BLACKBOARD_KEY_SET(BB_NEARBY_PLAYMATE), PROC_REF(on_playmate_find))
+ RegisterSignal(computer, COMSIG_ATOM_ENTERED, PROC_REF(on_pet_entered))
+ RegisterSignal(computer, COMSIG_ATOM_EXITED, PROC_REF(on_pet_exit))
+
+/datum/computer_file/program/virtual_pet/Destroy()
+ GLOB.virtual_pets_list -= src
+ if(!QDELETED(pet))
+ QDEL_NULL(pet)
+ STOP_PROCESSING(SSprocessing, src)
+ return ..()
+
+/datum/computer_file/program/virtual_pet/proc/on_death(datum/source)
+ SIGNAL_HANDLER
+
+ pet.forceMove(computer)
+
+
+/datum/computer_file/program/virtual_pet/proc/on_message_recieve(datum/source, sender_title, inbound_message, photo_message)
+ SIGNAL_HANDLER
+
+ var/message_to_display = "[sender_title] has sent you a message [photo_message ? "with a photo attached" : ""]: [inbound_message]!"
+ pet.ai_controller?.set_blackboard_key(BB_LAST_RECIEVED_MESSAGE, message_to_display)
+
+/datum/computer_file/program/virtual_pet/proc/pet_pre_clean(atom/source, mob/user)
+ SIGNAL_HANDLER
+
+ if(!COOLDOWN_FINISHED(src, on_clean_cooldown))
+ source.balloon_alert(user, "already clean!")
+ return COMSIG_ATOM_CANCEL_CLEAN
+
+/datum/computer_file/program/virtual_pet/proc/on_playmate_find(datum/source)
+ SIGNAL_HANDLER
+
+ happiness = min(happiness + PET_PLAYMATE_BONUS, max_happiness)
+ START_PROCESSING(SSprocessing, src)
+
+/datum/computer_file/program/virtual_pet/proc/post_cleaned(mob/source, mob/user)
+ SIGNAL_HANDLER
+
+ source.spin(spintime = 2 SECONDS, speed = 1) //celebrate!
+ happiness = min(happiness + PET_CLEAN_BONUS, max_happiness)
+ COOLDOWN_START(src, on_clean_cooldown, 1 MINUTES)
+ START_PROCESSING(SSprocessing, src)
+
+///manage the pet's hat offsets when he changes direction
+/datum/computer_file/program/virtual_pet/proc/on_change_dir(datum/source, old_dir, new_dir)
+ SIGNAL_HANDLER
+
+ if(!length(selected_hat))
+ return
+ set_hat_offsets(new_dir)
+
+/datum/computer_file/program/virtual_pet/proc/on_photo_captured(datum/source, atom/target, atom/user, datum/picture/photo)
+ SIGNAL_HANDLER
+
+ if(isnull(photo))
+ return
+ computer.store_file(new /datum/computer_file/picture(photo))
+
+/datum/computer_file/program/virtual_pet/proc/set_hat_offsets(new_dir)
+ var/direction_text = dir2text(new_dir)
+ var/list/offsets_list = hat_offsets[direction_text]
+ if(isnull(offsets_list))
+ return
+ var/mutable_appearance/hat_appearance = selected_hat["appearance"]
+ hat_appearance.pixel_x = offsets_list[1]
+ hat_appearance.pixel_y = offsets_list[2]
+ pet.update_appearance(UPDATE_OVERLAYS)
+
+///give our pet his hologram hat
+/datum/computer_file/program/virtual_pet/proc/on_overlays_updated(atom/source, list/overlays)
+ SIGNAL_HANDLER
+
+ if(!length(selected_hat))
+ return
+ overlays += selected_hat["appearance"]
+
+/datum/computer_file/program/virtual_pet/proc/alter_profile_picture()
+ var/image/pet_preview = image(icon = 'icons/ui_icons/virtualpet/pet_state.dmi', icon_state = "pet_preview")
+ if(LAZYACCESS(pet.atom_colours, FIXED_COLOUR_PRIORITY))
+ pet_preview.color = pet.atom_colours[FIXED_COLOUR_PRIORITY]
+
+ if(length(selected_hat))
+ var/mutable_appearance/our_selected_hat = selected_hat["appearance"]
+ var/mutable_appearance/hat_preview = mutable_appearance(our_selected_hat.icon, our_selected_hat.icon_state)
+ hat_preview.pixel_y = -9
+ pet_preview.add_overlay(hat_preview)
+
+ profile_picture = getFlatIcon(pet_preview)
+ COOLDOWN_START(src, alter_appearance_cooldown, 10 SECONDS)
+
+
+///decrease the pet's hunger after it eats
+/datum/computer_file/program/virtual_pet/proc/after_pet_eat(datum/source)
+ SIGNAL_HANDLER
+
+ hunger = min(hunger + PET_EAT_BONUS, max_hunger)
+ happiness = min(happiness + PET_EAT_BONUS, max_happiness)
+ START_PROCESSING(SSprocessing, src)
+
+///start processing if we enter the pda and need healing
+/datum/computer_file/program/virtual_pet/proc/on_pet_entered(atom/movable/source, atom/movable/arrived, atom/old_loc, list/atom/old_locs)
+ SIGNAL_HANDLER
+
+ if(arrived != pet)
+ return
+ ADD_TRAIT(pet, TRAIT_AI_PAUSED, REF(src))
+ if((datum_flags & DF_ISPROCESSING))
+ return
+ if(pet.health < pet.maxHealth) //if we're in the pda, heal up
+ START_PROCESSING(SSprocessing, src)
+
+/datum/computer_file/program/virtual_pet/proc/on_pet_exit(atom/movable/source, atom/movable/exited)
+ SIGNAL_HANDLER
+
+ if(exited != pet)
+ return
+ REMOVE_TRAIT(pet, TRAIT_AI_PAUSED, REF(src))
+ if((datum_flags & DF_ISPROCESSING))
+ return
+ if(hunger > 0 || happiness > 0) //if were outside the pda, we become hungry and happiness decreases
+ START_PROCESSING(SSprocessing, src)
+
+/datum/computer_file/program/virtual_pet/process()
+ if(pet.loc == computer)
+ if(pet.health >= pet.maxHealth)
+ return PROCESS_KILL
+ if(pet.stat == DEAD)
+ pet.revive(ADMIN_HEAL_ALL)
+ pet.heal_overall_damage(5)
+ return
+
+ if(hunger > 0)
+ hunger--
+
+ if(happiness > 0)
+ happiness--
+
+ if(hunger <=0 && happiness <= 0)
+ return PROCESS_KILL
+
+/datum/computer_file/program/virtual_pet/proc/after_pet_move(atom/movable/movable, atom/old_loc)
+ SIGNAL_HANDLER
+
+ if(!isturf(pet.loc) || !isturf(old_loc))
+ return
+ steps_counter = min(steps_counter + 1, PET_MAX_STEPS_RECORD)
+ increment_exp()
+ if(steps_counter % 2000 == 0) //every 2000 steps, announce the milestone to the world!
+ announce_global_updates(message = "has walked [steps_counter] steps!")
+
+/datum/computer_file/program/virtual_pet/proc/increment_exp()
+ var/modifier = 1
+ var/hunger_happiness = hunger + happiness
+ var/max_hunger_happiness = max_hunger + max_happiness
+
+ switch(hunger_happiness / max_hunger_happiness)
+ if(0.8 to 1)
+ modifier = 3
+ if(0.5 to 0.8)
+ modifier = 2
+
+ current_level_progress = min(current_level_progress + modifier, to_next_level)
+ if(current_level_progress >= to_next_level)
+ handle_level_up()
+
+/datum/computer_file/program/virtual_pet/proc/handle_level_up()
+ current_level_progress = 0
+ level++
+ grant_level_abilities()
+ pet.ai_controller?.set_blackboard_key(BB_VIRTUAL_PET_LEVEL, level)
+ playsound(computer.loc, 'sound/items/orbie_level_up.ogg', 50)
+ to_next_level += (level**2) + 500
+ SEND_SIGNAL(pet, COMSIG_VIRTUAL_PET_LEVEL_UP, level) //its a signal so different path types of virtual pets can handle leveling up differently
+ announce_global_updates(message = "has reached level [level]!")
+
+/datum/computer_file/program/virtual_pet/proc/grant_level_abilities()
+ switch(level)
+ if(2)
+ RegisterSignal(computer, COMSIG_COMPUTER_RECIEVED_MESSAGE, PROC_REF(on_message_recieve)) // we will now read out PDA messages
+ var/datum/action/cooldown/mob_cooldown/lights/lights = new(pet)
+ lights.Grant(pet)
+ pet.ai_controller?.set_blackboard_key(BB_LIGHTS_ABILITY, lights)
+ if(3)
+ var/datum/action/cooldown/mob_cooldown/capture_photo/photo_ability = new(pet)
+ photo_ability.Grant(pet)
+ pet.ai_controller?.set_blackboard_key(BB_PHOTO_ABILITY, photo_ability)
+ RegisterSignal(photo_ability.ability_camera, COMSIG_CAMERA_IMAGE_CAPTURED, PROC_REF(on_photo_captured))
+
+/datum/computer_file/program/virtual_pet/proc/announce_global_updates(message)
+ if(isnull(message))
+ return
+ var/list/message_to_announce = list(
+ "name" = pet.name,
+ "pet_picture" = icon2base64(profile_picture),
+ "message" = message,
+ "likers" = list(REF(src))
+ )
+ if(length(GLOB.global_pet_updates) >= MAX_UPDATE_LENGTH)
+ GLOB.global_pet_updates.Cut(1,2)
+
+ GLOB.global_pet_updates += list(message_to_announce)
+ playsound(computer.loc, 'sound/items/orbie_notification_sound.ogg', 50)
+
+/datum/computer_file/program/virtual_pet/proc/remove_pet(datum/source)
+ SIGNAL_HANDLER
+ pet = null
+ if(QDELETED(src))
+ return
+ computer.remove_file(src) //all is lost we no longer have a reason to exist
+
+/datum/computer_file/program/virtual_pet/kill_program(mob/user)
+ if(pet && pet.loc != computer)
+ pet.forceMove(computer) //recall the hologram back to the pda
+ STOP_PROCESSING(SSprocessing, src)
+ return ..()
+
+/datum/computer_file/program/virtual_pet/proc/get_pet_state()
+ if(isnull(pet))
+ return
+
+ if(pet.loc == computer)
+ return PET_STATE_ASLEEP
+
+ if(happiness/max_happiness > 0.8)
+ return PET_STATE_HAPPY
+
+ if(hunger/max_hunger < 0.5)
+ return PET_STATE_HUNGRY
+
+ return PET_STATE_NEUTRAL
+
+/datum/computer_file/program/virtual_pet/ui_data(mob/user)
+ var/list/data = list()
+ data["currently_summoned"] = (pet.loc != computer)
+ data["selected_area"] = (selected_area ? selected_area.name : "No location set")
+ data["pet_state"] = get_pet_state()
+ data["hunger"] = hunger
+ data["maximum_hunger"] = max_hunger
+ data["pet_hat"] = (length(selected_hat) ? selected_hat["name"] : "none")
+ data["can_reroll"] = COOLDOWN_FINISHED(src, area_reroll)
+ data["can_summon"] = COOLDOWN_FINISHED(src, summon_cooldown)
+ data["can_alter_appearance"] = COOLDOWN_FINISHED(src, alter_appearance_cooldown)
+ data["pet_name"] = pet.name
+ data["steps_counter"] = steps_counter
+ data["in_dropzone"] = (istype(get_area(computer), selected_area))
+ data["pet_area"] = (pet.loc != computer ? get_area_name(pet) : "Sleeping in PDA")
+ data["current_exp"] = current_level_progress
+ data["required_exp"] = to_next_level
+ data["happiness"] = happiness
+ data["maximum_happiness"] = max_happiness
+ data["level"] = level
+ data["pet_color"] = ""
+
+ var/color_value = LAZYACCESS(pet.atom_colours, FIXED_COLOUR_PRIORITY)
+ for(var/index in possible_colors)
+ if(possible_colors[index] == color_value)
+ data["pet_color"] = index
+ break
+
+ data["pet_gender"] = pet.gender
+
+ data["pet_updates"] = list()
+
+ for(var/i in length(GLOB.global_pet_updates) to 1 step -1)
+ var/list/update = GLOB.global_pet_updates[i]
+
+ if(isnull(update))
+ continue
+
+ data["pet_updates"] += list(list(
+ "update_id" = i,
+ "update_name" = update["name"],
+ "update_picture" = update["pet_picture"],
+ "update_message" = update["message"],
+ "update_likers" = length(update["likers"]),
+ "update_already_liked" = ((REF(src)) in update["likers"]),
+ ))
+
+ data["all_pets"] = list()
+ for(var/datum/computer_file/program/virtual_pet/program as anything in GLOB.virtual_pets_list)
+ data["all_pets"] += list(list(
+ "other_pet_name" = program.pet.name,
+ "other_pet_picture" = icon2base64(program.profile_picture),
+ ))
+ return data
+
+/datum/computer_file/program/virtual_pet/ui_static_data(mob/user)
+ var/list/data = list()
+ data["pet_state_icons"] = list()
+ for(var/list_index as anything in pet_state_icons)
+ var/list/sprite_location = pet_state_icons[list_index]
+ data["pet_state_icons"] += list(list(
+ "name" = list_index,
+ "icon" = icon2base64(getFlatIcon(image(icon = sprite_location["icon"], icon_state = sprite_location["icon_state"]), no_anim=TRUE))
+ ))
+
+ data["hat_selections"] = list(list(
+ "hat_id" = null,
+ "hat_name" = "none",
+ ))
+
+ for(var/type_index as anything in hat_selections)
+ if(level >= hat_selections[type_index])
+ var/obj/item/hat = type_index
+ data["hat_selections"] += list(list(
+ "hat_id" = type_index,
+ "hat_name" = initial(hat.name),
+ ))
+
+ data["possible_colors"] = list()
+ for(var/color in possible_colors)
+ data["possible_colors"] += list(list(
+ "color_name" = color,
+ "color_value" = possible_colors[color],
+ ))
+
+ var/static/list/possible_emotes = list(
+ /datum/emote/flip,
+ /datum/emote/living/jump,
+ /datum/emote/living/shiver,
+ /datum/emote/spin,
+ /datum/emote/living/beep,
+ )
+ data["possible_emotes"] = list("none")
+ for(var/datum/emote/target_emote as anything in possible_emotes)
+ data["possible_emotes"] += target_emote.key
+
+ data["preview_icon"] = icon2base64(profile_picture)
+ return data
+
+/datum/computer_file/program/virtual_pet/ui_act(action, params, datum/tgui/ui)
+ . = ..()
+ switch(action)
+
+ if("summon_pet")
+ if(!COOLDOWN_FINISHED(src, summon_cooldown))
+ return TRUE
+ if(pet.loc == computer)
+ release_pet(ui.user)
+ else
+ recall_pet()
+ COOLDOWN_START(src, summon_cooldown, 10 SECONDS)
+
+ if("apply_customization")
+ if(!COOLDOWN_FINISHED(src, alter_appearance_cooldown))
+ return TRUE
+ var/obj/item/chosen_type = text2path(params["chosen_hat"])
+ if(isnull(chosen_type))
+ selected_hat.Cut()
+
+ else if((chosen_type in hat_selections))
+ selected_hat["name"] = initial(chosen_type.name)
+ var/mutable_appearance/selected_hat_appearance = mutable_appearance(icon = initial(chosen_type.worn_icon), icon_state = initial(chosen_type.icon_state), layer = ABOVE_ALL_MOB_LAYER)
+ selected_hat_appearance.transform = selected_hat_appearance.transform.Scale(0.8, 1)
+ selected_hat["appearance"] = selected_hat_appearance
+ set_hat_offsets(pet.dir)
+
+ var/chosen_color = params["chosen_color"]
+ if(isnull(chosen_color))
+ pet.remove_atom_colour(FIXED_COLOUR_PRIORITY)
+ else
+ pet.add_atom_colour(chosen_color, FIXED_COLOUR_PRIORITY)
+
+ var/input_name = sanitize_name(params["chosen_name"], allow_numbers = TRUE)
+ pet.name = (input_name ? input_name : initial(pet.name))
+ new /obj/effect/temp_visual/guardian/phase(pet.loc)
+
+ switch(params["chosen_gender"])
+ if("male")
+ pet.gender = MALE
+ if("female")
+ pet.gender = FEMALE
+ if("neuter")
+ pet.gender = NEUTER
+
+ pet.update_appearance()
+ alter_profile_picture()
+ update_static_data(ui.user, ui)
+
+ if("get_feed_location")
+ generate_petfeed_area()
+
+ if("drop_feed")
+ drop_feed()
+
+ if("like_update")
+ var/index = params["update_reference"]
+ var/list/update_message = GLOB.global_pet_updates[index]
+ if(isnull(update_message))
+ return TRUE
+ var/our_reference = REF(src)
+ if(our_reference in update_message["likers"])
+ update_message["likers"] -= our_reference
+ else
+ update_message["likers"] += our_reference
+
+ if("teach_tricks")
+ var/trick_name = params["trick_name"]
+ var/list/trick_sequence = params["tricks"]
+ if(isnull(pet.ai_controller))
+ return TRUE
+ if(!isnull(trick_name))
+ pet.ai_controller.set_blackboard_key(BB_TRICK_NAME, trick_name)
+ pet.ai_controller.override_blackboard_key(BB_TRICK_SEQUENCE, trick_sequence)
+ playsound(computer.loc, 'sound/items/orbie_trick_learned.ogg', 50)
+
+ return TRUE
+
+/datum/computer_file/program/virtual_pet/proc/generate_petfeed_area()
+ if(!COOLDOWN_FINISHED(src, area_reroll))
+ return
+ var/list/filter_area_list = typecache_filter_list(GLOB.the_station_areas, restricted_areas)
+ var/list/target_area_list = GLOB.the_station_areas.Copy() - filter_area_list
+ if(!length(target_area_list))
+ return
+ selected_area = pick(target_area_list)
+ COOLDOWN_START(src, area_reroll, 2 MINUTES)
+
+/datum/computer_file/program/virtual_pet/proc/drop_feed()
+ if(!istype(get_area(computer), selected_area))
+ return
+ announce_global_updates(message = "has found a chocolate at [selected_area.name]")
+ selected_area = null
+ var/obj/item/food/virtual_chocolate/chocolate = new(get_turf(computer))
+ chocolate.AddElement(/datum/element/temporary_atom, life_time = 30 SECONDS) //we cant maintain its existence for too long!
+
+/datum/computer_file/program/virtual_pet/proc/recall_pet()
+ animate(pet, transform = matrix().Scale(0.3, 0.3), time = 1.5 SECONDS)
+ addtimer(CALLBACK(pet, TYPE_PROC_REF(/atom/movable, forceMove), computer), 1.5 SECONDS)
+
+/datum/computer_file/program/virtual_pet/proc/release_pet(mob/living/our_user)
+ var/turf/drop_zone
+ var/list/turfs_list = get_adjacent_open_turfs(computer.drop_location())
+ for(var/turf/possible_turf as anything in turfs_list)
+ if(possible_turf.is_blocked_turf())
+ continue
+ drop_zone = possible_turf
+ break
+ var/turf/final_turf = isnull(drop_zone) ? computer.drop_location() : drop_zone
+ pet.befriend(our_user) //befriend whoever set us out
+ animate(pet, transform = matrix(), time = 1.5 SECONDS)
+ pet.forceMove(final_turf)
+ playsound(computer.loc, 'sound/items/orbie_send_out.ogg', 20)
+ new /obj/effect/temp_visual/guardian/phase(pet.loc)
+
+#undef PET_MAX_LEVEL
+#undef PET_MAX_STEPS_RECORD
+#undef PET_EAT_BONUS
+#undef PET_CLEAN_BONUS
+#undef PET_PLAYMATE_BONUS
+#undef PET_STATE_HUNGRY
+#undef PET_STATE_ASLEEP
+#undef PET_STATE_HAPPY
+#undef PET_STATE_NEUTRAL
+#undef MAX_UPDATE_LENGTH
diff --git a/code/modules/photography/camera/camera.dm b/code/modules/photography/camera/camera.dm
index 3f721c1cefc..4bdb1c4d93a 100644
--- a/code/modules/photography/camera/camera.dm
+++ b/code/modules/photography/camera/camera.dm
@@ -229,7 +229,7 @@
var/datum/picture/picture = new("picture", desc.Join(" "), mobs_spotted, dead_spotted, names, get_icon, null, psize_x, psize_y, blueprints, can_see_ghosts = see_ghosts)
after_picture(user, picture)
- SEND_SIGNAL(src, COMSIG_CAMERA_IMAGE_CAPTURED, target, user)
+ SEND_SIGNAL(src, COMSIG_CAMERA_IMAGE_CAPTURED, target, user, picture)
blending = FALSE
return picture
diff --git a/icons/mob/simple/pets.dmi b/icons/mob/simple/pets.dmi
index 9bd7d69c06b..311fff1e6b0 100644
Binary files a/icons/mob/simple/pets.dmi and b/icons/mob/simple/pets.dmi differ
diff --git a/icons/obj/food/food.dmi b/icons/obj/food/food.dmi
index c4d93c23b0b..2fb08c78be7 100644
Binary files a/icons/obj/food/food.dmi and b/icons/obj/food/food.dmi differ
diff --git a/icons/ui_icons/virtualpet/pet_state.dmi b/icons/ui_icons/virtualpet/pet_state.dmi
new file mode 100644
index 00000000000..7ec865d104b
Binary files /dev/null and b/icons/ui_icons/virtualpet/pet_state.dmi differ
diff --git a/modular_nova/master_files/code/datums/emotes.dm b/modular_nova/master_files/code/datums/emotes.dm
index 0b8b7d80e44..10511a6f827 100644
--- a/modular_nova/master_files/code/datums/emotes.dm
+++ b/modular_nova/master_files/code/datums/emotes.dm
@@ -1,2 +1,8 @@
+/datum/emote
+ /// Emote volume
+ var/sound_volume = 25
+ /// What species can use this emote?
+ var/list/allowed_species
+
/datum/emote/proc/check_config()
return TRUE
diff --git a/modular_nova/modules/emote_panel/code/emote_panel.dm b/modular_nova/modules/emote_panel/code/emote_panel.dm
index e5cb3f44d44..5acc788ce9c 100644
--- a/modular_nova/modules/emote_panel/code/emote_panel.dm
+++ b/modular_nova/modules/emote_panel/code/emote_panel.dm
@@ -98,7 +98,7 @@
all_emotes += human_emotes
// modular_nova\modules\emotes\code\emote.dm
- var/static/list/skyrat_living_emotes = list(
+ var/static/list/nova_living_emotes = list(
/mob/living/proc/emote_peep,
/mob/living/proc/emote_peep2,
/mob/living/proc/emote_snap,
@@ -148,7 +148,7 @@
/mob/living/proc/emote_moo,
/mob/living/proc/emote_honk1
)
- all_emotes += skyrat_living_emotes
+ all_emotes += nova_living_emotes
// code\modules\mob\living\brain\emote.dm
var/static/list/brain_emotes = list(
@@ -191,7 +191,7 @@
)
// modular_nova\modules\emotes\code\additionalemotes\overlay_emote.dm
- var/static/list/skyrat_living_emotes_overlay = list(
+ var/static/list/nova_living_emotes_overlay = list(
/mob/living/proc/emote_sweatdrop,
/mob/living/proc/emote_exclaim,
/mob/living/proc/emote_question,
@@ -199,7 +199,7 @@
/mob/living/proc/emote_annoyed,
/mob/living/proc/emote_glasses
)
- all_emotes += skyrat_living_emotes_overlay
+ all_emotes += nova_living_emotes_overlay
// modular_nova\modules\emotes\code\additionalemotes\turf_emote.dm
all_emotes += /mob/living/proc/emote_mark_turf
@@ -222,8 +222,8 @@
available_emotes += mob_emotes
if(isliving(src))
available_emotes += living_emotes
- available_emotes += skyrat_living_emotes
- available_emotes += skyrat_living_emotes_overlay
+ available_emotes += nova_living_emotes
+ available_emotes += nova_living_emotes_overlay
available_emotes += /mob/living/proc/emote_mark_turf
if(iscarbon(src))
available_emotes += carbon_emotes
diff --git a/modular_nova/modules/emotes/code/synth_emotes.dm b/modular_nova/modules/emotes/code/synth_emotes.dm
index 8c0bce5b963..13f9f376e61 100644
--- a/modular_nova/modules/emotes/code/synth_emotes.dm
+++ b/modular_nova/modules/emotes/code/synth_emotes.dm
@@ -7,7 +7,6 @@
message = "chirps happily!"
vary = TRUE
sound = 'modular_nova/modules/emotes/sound/emotes/dwoop.ogg'
- silicon_allowed = TRUE
allowed_species = list(/datum/species/synthetic)
cooldown = 2 SECONDS
@@ -16,7 +15,6 @@
message = "emits an affirmative blip."
vary = TRUE
sound = 'modular_nova/modules/emotes/sound/emotes/synth_yes.ogg'
- silicon_allowed = TRUE
allowed_species = list(/datum/species/synthetic)
cooldown = 2 SECONDS
@@ -25,7 +23,6 @@
message = "emits a negative blip."
vary = TRUE
sound = 'modular_nova/modules/emotes/sound/emotes/synth_no.ogg'
- silicon_allowed = TRUE
allowed_species = list(/datum/species/synthetic)
cooldown = 2 SECONDS
@@ -33,7 +30,6 @@
key = "boop"
key_third_person = "boops"
message = "boops."
- silicon_allowed = TRUE
allowed_species = list(/datum/species/synthetic)
cooldown = 2 SECONDS
@@ -44,20 +40,16 @@
message_param = "buzzes at %t."
emote_type = EMOTE_AUDIBLE
sound = 'sound/machines/buzz-sigh.ogg'
- silicon_allowed = TRUE
allowed_species = list(/datum/species/synthetic)
cooldown = 2 SECONDS
-/datum/emote/living/human/beep
- key = "beep"
- key_third_person = "beeps"
- message = "beeps."
- message_param = "beeps at %t."
- emote_type = EMOTE_AUDIBLE
- sound = 'sound/machines/twobeep.ogg'
- silicon_allowed = TRUE
+// This one is special, since it comes from TG and can be used by some basic mobs too. Handle it modularly.
+/datum/emote/living/beep
allowed_species = list(/datum/species/synthetic)
- cooldown = 2 SECONDS
+
+/datum/emote/living/beep/New()
+ mob_type_allowed_typecache += list(/mob/living/carbon/human)
+ return ..()
/datum/emote/living/human/beep2
key = "beep2"
@@ -67,7 +59,6 @@
emote_type = EMOTE_AUDIBLE
vary = TRUE
sound = 'modular_nova/modules/emotes/sound/emotes/twobeep.ogg'
- silicon_allowed = TRUE
allowed_species = list(/datum/species/synthetic)
cooldown = 2 SECONDS
@@ -76,7 +67,6 @@
message = "buzzes twice."
emote_type = EMOTE_AUDIBLE
sound = 'sound/machines/buzz-two.ogg'
- silicon_allowed = TRUE
allowed_species = list(/datum/species/synthetic)
cooldown = 2 SECONDS
@@ -86,7 +76,6 @@
message = "chimes."
emote_type = EMOTE_AUDIBLE
sound = 'sound/machines/chime.ogg'
- silicon_allowed = TRUE
allowed_species = list(/datum/species/synthetic)
cooldown = 2 SECONDS
@@ -97,7 +86,6 @@
emote_type = EMOTE_AUDIBLE
vary = TRUE
sound = 'sound/items/bikehorn.ogg'
- silicon_allowed = TRUE
allowed_species = list(/datum/species/synthetic)
cooldown = 2 SECONDS
@@ -108,7 +96,6 @@
message_param = "pings at %t."
emote_type = EMOTE_AUDIBLE
sound = 'sound/machines/ping.ogg'
- silicon_allowed = TRUE
allowed_species = list(/datum/species/synthetic)
cooldown = 2 SECONDS
@@ -117,7 +104,6 @@
message = "plays a sad trombone..."
emote_type = EMOTE_AUDIBLE
sound = 'sound/misc/sadtrombone.ogg'
- silicon_allowed = TRUE
allowed_species = list(/datum/species/synthetic)
cooldown = 2 SECONDS
@@ -126,7 +112,6 @@
message = "blares an alarm!"
emote_type = EMOTE_AUDIBLE
sound = 'sound/machines/warning-buzzer.ogg'
- silicon_allowed = TRUE
allowed_species = list(/datum/species/synthetic)
cooldown = 2 SECONDS
@@ -135,7 +120,6 @@
message = "activates their slow clap processor."
emote_type = EMOTE_AUDIBLE
sound = 'sound/machines/slowclap.ogg'
- silicon_allowed = TRUE
allowed_species = list(/datum/species/synthetic)
cooldown = 2 SECONDS
@@ -144,6 +128,5 @@
message = "plays a laughtrack."
emote_type = EMOTE_AUDIBLE
sound = 'sound/items/sitcomlaugh2.ogg'
- silicon_allowed = TRUE
allowed_species = list(/datum/species/synthetic)
cooldown = 2 SECONDS
diff --git a/sound/items/orbie_level_up.ogg b/sound/items/orbie_level_up.ogg
new file mode 100644
index 00000000000..c876c9d7817
Binary files /dev/null and b/sound/items/orbie_level_up.ogg differ
diff --git a/sound/items/orbie_notification_sound.ogg b/sound/items/orbie_notification_sound.ogg
new file mode 100644
index 00000000000..b43bba41ae5
Binary files /dev/null and b/sound/items/orbie_notification_sound.ogg differ
diff --git a/sound/items/orbie_send_out.ogg b/sound/items/orbie_send_out.ogg
new file mode 100644
index 00000000000..aba3d84e186
Binary files /dev/null and b/sound/items/orbie_send_out.ogg differ
diff --git a/sound/items/orbie_trick_learned.ogg b/sound/items/orbie_trick_learned.ogg
new file mode 100644
index 00000000000..bc50cf41b1c
Binary files /dev/null and b/sound/items/orbie_trick_learned.ogg differ
diff --git a/tgstation.dme b/tgstation.dme
index 1c340f63dac..f5023ac9b57 100644
--- a/tgstation.dme
+++ b/tgstation.dme
@@ -4790,6 +4790,9 @@
#include "code\modules\mob\living\basic\pets\dog\corgi.dm"
#include "code\modules\mob\living\basic\pets\dog\dog_subtypes.dm"
#include "code\modules\mob\living\basic\pets\dog\strippable_items.dm"
+#include "code\modules\mob\living\basic\pets\orbie\orbie.dm"
+#include "code\modules\mob\living\basic\pets\orbie\orbie_abilities.dm"
+#include "code\modules\mob\living\basic\pets\orbie\orbie_ai.dm"
#include "code\modules\mob\living\basic\pets\parrot\_parrot.dm"
#include "code\modules\mob\living\basic\pets\parrot\parrot_items.dm"
#include "code\modules\mob\living\basic\pets\parrot\parrot_subtypes.dm"
@@ -5200,6 +5203,7 @@
#include "code\modules\modular_computers\file_system\programs\statusdisplay.dm"
#include "code\modules\modular_computers\file_system\programs\techweb.dm"
#include "code\modules\modular_computers\file_system\programs\theme_selector.dm"
+#include "code\modules\modular_computers\file_system\programs\virtual_pet.dm"
#include "code\modules\modular_computers\file_system\programs\wirecarp.dm"
#include "code\modules\modular_computers\file_system\programs\antagonist\contractor_program.dm"
#include "code\modules\modular_computers\file_system\programs\antagonist\dos.dm"
diff --git a/tgui/packages/tgui/interfaces/NtosVirtualPet.tsx b/tgui/packages/tgui/interfaces/NtosVirtualPet.tsx
new file mode 100644
index 00000000000..7b61f2b566e
--- /dev/null
+++ b/tgui/packages/tgui/interfaces/NtosVirtualPet.tsx
@@ -0,0 +1,527 @@
+import { BooleanLike } from 'common/react';
+import { capitalize } from 'common/string';
+import { useState } from 'react';
+
+import { useBackend } from '../backend';
+import {
+ Box,
+ Button,
+ Dropdown,
+ Flex,
+ Image,
+ Input,
+ LabeledList,
+ ProgressBar,
+ Section,
+ Stack,
+ Tabs,
+} from '../components';
+import { NtosWindow } from '../layouts';
+
+type Data = {
+ currently_summoned: BooleanLike;
+ pet_state: string;
+ hunger: number;
+ current_exp: number;
+ steps_counter: number;
+ required_exp: number;
+ happiness: number;
+ pet_area: string;
+ maximum_happiness: number;
+ maximum_hunger: number;
+ level: number;
+ preview_icon: string;
+ pet_color: string;
+ pet_hat: string;
+ pet_name: string;
+ selected_area: string;
+ can_reroll: BooleanLike;
+ pet_gender: string;
+ in_dropzone: BooleanLike;
+ can_summon: BooleanLike;
+ can_alter_appearance: BooleanLike;
+ possible_emotes: string[];
+ pet_state_icons: Pet_State_Icons[];
+ hat_selections: Hat_Selections[];
+ possible_colors: Possible_Colors[];
+ pet_updates: Pet_Updates[];
+};
+
+type Pet_State_Icons = {
+ name: string;
+ icon: string;
+};
+
+type Hat_Selections = {
+ hat_id: string;
+ hat_name: string;
+};
+
+type Possible_Colors = {
+ color_name: string;
+ color_value: string;
+};
+
+type Pet_Updates = {
+ update_id: number;
+ update_name: string;
+ update_picture: string;
+ update_message: string;
+ update_likers: number;
+ update_already_liked: BooleanLike;
+};
+
+enum Tab {
+ Stats,
+ Customization,
+ Updates,
+ Tricks,
+}
+
+enum PetGender {
+ male = 'male',
+ female = 'female',
+ neuter = 'neuter',
+}
+
+export const NtosVirtualPet = (props) => {
+ const [tab, setTab] = useState(Tab.Stats);
+
+ return (
+
+
+
+ setTab(Tab.Stats)}
+ >
+ Stats
+
+ setTab(Tab.Customization)}
+ >
+ Customization
+
+ setTab(Tab.Updates)}
+ >
+ Pet Updates
+
+ setTab(Tab.Tricks)}
+ >
+ Tricks
+
+
+ {tab === Tab.Stats && }
+ {tab === Tab.Customization && }
+ {tab === Tab.Updates && }
+ {tab === Tab.Tricks && }
+
+
+ );
+};
+
+const Stats = (props) => {
+ const { act, data } = useBackend();
+ const {
+ currently_summoned,
+ pet_state,
+ hunger,
+ current_exp,
+ required_exp,
+ happiness,
+ maximum_happiness,
+ maximum_hunger,
+ level,
+ pet_area,
+ steps_counter,
+ selected_area,
+ can_reroll,
+ can_summon,
+ in_dropzone,
+ } = data;
+ return (
+ <>
+
+
+
+
+
+
+
+ Current Level: {level}
+
+ Happiness:
+
+
+
+ Exp Progress:
+
+
+
+ Hunger:
+
+
+
+
+
+
+
+
+ {pet_area}
+
+
+
+
+
+
+
+
+
+ {selected_area}
+
+ {(in_dropzone && (
+
+ )) || (
+
+ )}
+
+
+
+
+
+
+ {' '}
+ Steps: {steps_counter}
+
+
+
+ >
+ );
+};
+
+const PetTricks = (props) => {
+ const { act, data } = useBackend();
+ const { possible_emotes } = data;
+ const [sequences, setSequences] = useState(['none', 'none', 'none', 'none']);
+ const [TrickName, setTrickName] = useState('Trick');
+
+ const UpdateSequence = (Index: number, Trick: string) => {
+ const NewSequence = [...sequences];
+ NewSequence[Index] = Trick;
+ setSequences(NewSequence);
+ };
+
+ return (
+ setTrickName(value)}
+ >
+ Rename Trick
+
+ }
+ >
+
+ {sequences.map((sequence, index) => (
+
+ UpdateSequence(index, selected)}
+ />
+
+ ))}
+
+
+
+ );
+};
+
+const Customization = (props) => {
+ const { act, data } = useBackend();
+ const {
+ preview_icon,
+ hat_selections = [],
+ possible_colors = [],
+ pet_hat,
+ pet_color,
+ pet_name,
+ pet_gender,
+ can_alter_appearance,
+ } = data;
+
+ const hatSelectionList = {};
+ for (const index in hat_selections) {
+ const hat = hat_selections[index];
+ hatSelectionList[hat.hat_name] = hat;
+ }
+
+ const possibleColorList = {};
+ for (const index in possible_colors) {
+ const color = possible_colors[index];
+ possibleColorList[color.color_name] = color;
+ }
+
+ const [selectedHat, setSelectedHat] = useState(hatSelectionList[pet_hat]);
+ const [selectedGender, setSelectedGender] = useState(pet_gender);
+ const [selectedName, setSelectedName] = useState(pet_name);
+ const [selectedColor, setSelectedColor] = useState(
+ possibleColorList[pet_color],
+ );
+ return (
+ <>
+
+
+
+
+
+
+
+ {
+ return selected_hat.hat_name;
+ })}
+ onSelected={(selected) =>
+ setSelectedHat(hatSelectionList[selected])
+ }
+ />
+
+
+
+
+
+
+ {
+ return possible_color.color_name;
+ })}
+ onSelected={(selected) =>
+ setSelectedColor(possibleColorList[selected])
+ }
+ />
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ >
+ );
+};
+
+const AllPetUpdates = (props) => {
+ const { act, data } = useBackend();
+ const { pet_updates } = data;
+
+ return (
+
+
+ {pet_updates.map((update) => (
+
+
+
+
+
+
+ {update.update_name.substring(0, 6)}
+
+
+
+
+
+
+
+ {update.update_name.substring(0, 6)} {update.update_message}
+
+
+
+
+
+ ))}
+
+
+ );
+};
+
+const PetIcon = (props) => {
+ const { data } = useBackend();
+ const { pet_state_icons = [] } = data;
+ const { our_pet_state } = props;
+
+ let icon_display = pet_state_icons.find(
+ (pet_icon) => pet_icon.name === our_pet_state,
+ );
+
+ if (!icon_display) {
+ return null;
+ }
+
+ return (
+
+
+
+
+ {capitalize(our_pet_state)}
+
+ );
+};