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 ( + <> +
+ +
+ + +
+ setSelectedName(value)} + /> +
+
+ +
+ { + 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)} + + ); +};