## About The Pull Request
adds snails to the game

![image](https://github.com/user-attachments/assets/f1745795-695f-443a-827e-1f1987d4fdb6)

these are harmless critters you can find in maints. they love eating all
variety of fruits, and are gravitated towards snail people, where
they'll dance around them.

you can also pick them up and put them on your head.

finally, you can also grab them and put them in hydrotrays (they will
swim around in it very slowly). they'll help ur plants grow as they act
as natural weed-repellants, as they'll eat weeds that grow in trays.

![image](https://github.com/user-attachments/assets/512f0974-d0d1-4f90-a540-c03d1c969328)


## Why It's Good For The Game
there's not that many mobs you can usually find in maints, currently
there's only mice and cockroaches, this helps expand the pools a bit.

## Changelog
🆑
add: adds snails to the game. (keep them away from salt!)
/🆑
This commit is contained in:
Ben10Omintrix
2025-05-05 15:29:14 +03:00
committed by GitHub
parent ab738a10c3
commit a708c8e8da
18 changed files with 328 additions and 12 deletions

View File

@@ -326,3 +326,7 @@
#define BB_TURTLE_FLORA_TARGET "turtle_flora_target"
#define BB_GUNMIMIC_GUN_EMPTY "BB_GUNMIMIC_GUN_EMPTY"
//snails
///snails retreat ability
#define BB_SNAIL_RETREAT_ABILITY "snail_retreat_ability"

View File

@@ -1493,4 +1493,6 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai
/// Demolition modifier when hitting this object is inverted (ie, 1 / demolition)
#define TRAIT_INVERTED_DEMOLITION "demolition_inverted"
/// Trait given when we escape into our shell
#define TRAIT_SHELL_RETREATED "shell_retreated"
// END TRAIT DEFINES

View File

@@ -493,6 +493,7 @@ GLOBAL_LIST_INIT(traits_by_type, list(
"TRAIT_SELF_AWARE" = TRAIT_SELF_AWARE,
"TRAIT_SETTLER" = TRAIT_SETTLER,
"TRAIT_SHAVED" = TRAIT_SHAVED,
"TRAIT_SHELL_RETREATED" = TRAIT_SHELL_RETREATED,
"TRAIT_SHIFTY_EYES" = TRAIT_SHIFTY_EYES,
"TRAIT_SHOCKIMMUNE" = TRAIT_SHOCKIMMUNE,
"TRAIT_SIGHT_BYPASS" = TRAIT_SIGHT_BYPASS,

View File

@@ -1,4 +1,3 @@
#define PROB_MOUSE_SPAWN 98
#define PROB_SPIDER_REPLACEMENT 50
SUBSYSTEM_DEF(minor_mapping)
@@ -8,6 +7,12 @@ SUBSYSTEM_DEF(minor_mapping)
/datum/controller/subsystem/atoms,
)
flags = SS_NO_FIRE
///a list of vermin we pick from to spawn.
var/list/vermin_chances = list(
/mob/living/basic/mouse = 80,
/mob/living/basic/snail = 18,
/mob/living/basic/regal_rat/controlled = 2,
)
/datum/controller/subsystem/minor_mapping/Initialize()
// This whole subsystem just introduces a lot of odd confounding variables into unit test situations,
@@ -30,14 +35,14 @@ SUBSYSTEM_DEF(minor_mapping)
continue
to_spawn--
if(HAS_TRAIT(SSstation, STATION_TRAIT_SPIDER_INFESTATION) && prob(PROB_SPIDER_REPLACEMENT))
new /mob/living/basic/spider/maintenance(proposed_turf)
return
if (prob(PROB_MOUSE_SPAWN))
new /mob/living/basic/mouse(proposed_turf)
var/picked_path
if(HAS_TRAIT(SSstation, STATION_TRAIT_SPIDER_INFESTATION) && prob(PROB_SPIDER_REPLACEMENT))
picked_path = /mob/living/basic/spider/maintenance
else
new /mob/living/basic/regal_rat/controlled(proposed_turf)
picked_path = pick_weight(vermin_chances)
new picked_path(proposed_turf)
/// Returns true if a mouse won't die if spawned on this turf
/datum/controller/subsystem/minor_mapping/proc/valid_mouse_turf(turf/open/proposed_turf)
@@ -91,5 +96,4 @@ SUBSYSTEM_DEF(minor_mapping)
return shuffle(suitable)
#undef PROB_MOUSE_SPAWN
#undef PROB_SPIDER_REPLACEMENT

View File

@@ -56,6 +56,10 @@
enemies_list += potential_target
if(!length(enemies_list))
if(existing_target)
controller.clear_blackboard_key(target_key)
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
var/atom/new_target = pick_final_target(controller, enemies_list)

View File

@@ -45,6 +45,7 @@
/obj/item/reagent_containers/cup/soda_cans/grey_bull = 1,
/obj/effect/spawner/random/engineering/tool = 1,
/mob/living/basic/mouse = 1,
/mob/living/basic/snail = 1,
/obj/item/food/grown/cannabis = 1,
/obj/item/reagent_containers/cup/rag = 1,
/obj/effect/spawner/random/entertainment/drugs= 1,

View File

@@ -79,6 +79,7 @@
/mob/living/basic/cockroach,
/mob/living/basic/spider/maintenance,
/mob/living/basic/mouse,
/mob/living/basic/snail,
)
return pick(mob_list)

View File

@@ -32,7 +32,7 @@
description = "A scientist needs vermin to test on, use the cytology equipment to grow some of these simple critters!"
total_requirement = 3
max_requirement_per_type = 2
possible_types = list(/mob/living/basic/cockroach, /mob/living/basic/mouse)
possible_types = list(/mob/living/basic/cockroach, /mob/living/basic/mouse, /mob/living/basic/snail)
/datum/experiment/scanning/random/cytology/medium
name = "Advanced Cytology Scanning Experiment"

View File

@@ -59,6 +59,8 @@
var/being_pollinated = FALSE
///The light level on the tray tile
var/light_level = 0
///our snail overlay, if any
var/obj/effect/overlay/vis_effect/snail/our_snail
/obj/machinery/hydroponics/Initialize(mapload)
//ALRIGHT YOU DEGENERATES. YOU HAD REAGENT HOLDERS FOR AT LEAST 4 YEARS AND NONE OF YOU MADE HYDROPONICS TRAYS HOLD NUTRIENT CHEMS INSTEAD OF USING "Points".
@@ -212,6 +214,16 @@
. = ..()
if(!QDELETED(src) && gone == myseed)
set_seed(null, FALSE)
if(!istype(gone, /obj/item/clothing/head/mob_holder/snail))
return
var/obj/item/clothing/head/mob_holder/snail_object = gone
if(snail_object.held_mob)
UnregisterSignal(snail_object.held_mob, list(
COMSIG_LIVING_DEATH,
COMSIG_MOVABLE_ATTEMPTED_MOVE,
))
QDEL_NULL(our_snail)
/obj/machinery/hydroponics/constructable/attackby(obj/item/I, mob/living/user, list/modifiers)
if (!user.combat_mode)
@@ -313,6 +325,9 @@
/obj/machinery/hydroponics/process(seconds_per_tick)
var/needs_update = FALSE // Checks if the icon needs updating so we don't redraw empty trays every time
if(!isnull(our_snail))
handle_snail()
if(self_sustaining)
if(powered())
adjust_waterlevel(rand(1,2) * seconds_per_tick * 0.5)
@@ -646,6 +661,17 @@
/obj/machinery/hydroponics/proc/adjust_pestlevel(amt)
set_pestlevel(clamp(pestlevel + amt, 0, MAX_TRAY_PESTS), FALSE)
/obj/machinery/hydroponics/proc/remove_snail(mob/source)
SIGNAL_HANDLER
var/atom/movable/mob_holder = source.loc
mob_holder.forceMove(drop_location())
/obj/machinery/hydroponics/proc/handle_snail()
if(prob(15))
our_snail.handle_animation() //our snail waddles throughout the tray
if(prob(5))
adjust_weedlevel(-2)
/**
* Adjust Weeds.
@@ -1110,11 +1136,17 @@
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
var/warning = tgui_alert(user, "Are you sure you wish to empty the tray's nutrient beaker?","Empty Tray Nutrients?", list("Yes", "No"))
if(warning == "Yes" && user.can_perform_action(src, FORBID_TELEKINESIS_REACH))
reagents.clear_reagents()
to_chat(user, span_warning("You empty [src]'s nutrient tank."))
empty_tray(user)
update_appearance()
return SECONDARY_ATTACK_CANCEL_ATTACK_CHAIN
/obj/machinery/hydroponics/proc/empty_tray(mob/user)
reagents.clear_reagents()
for(var/obj/item/clothing/head/mob_holder/snail/possible_snail in contents)
possible_snail.forceMove(drop_location())
to_chat(user, span_warning("You empty [src]'s nutrient tank."))
/**
* Update Tray Proc
* Handles plant harvesting on the tray side, by clearing the seed, names, description, and dead stat.
@@ -1243,6 +1275,17 @@
plant_health = add_output_port("Plant Health", PORT_TYPE_NUMBER)
reagents_level = add_output_port("Reagents Level", PORT_TYPE_NUMBER)
/obj/machinery/hydroponics/Entered(atom/movable/arrived, atom/old_loc, list/atom/old_locs)
. = ..()
if(!istype(arrived, /obj/item/clothing/head/mob_holder/snail))
return
our_snail = new
vis_contents += our_snail
our_snail.layer = layer + 0.01
var/obj/item/clothing/head/mob_holder/snail = arrived
RegisterSignals(snail.held_mob, list(COMSIG_MOVABLE_ATTEMPTED_MOVE, COMSIG_LIVING_DEATH), PROC_REF(remove_snail)) //rip
/obj/item/circuit_component/hydroponics/register_usb_parent(atom/movable/parent)
. = ..()
if(istype(parent, /obj/machinery/hydroponics))

View File

@@ -0,0 +1,22 @@
#define SNAIL_MOVEMENT_DISTANCE 5
#define SNAIL_MOVEMENT_TIME 10 SECONDS
/obj/effect/overlay/vis_effect/snail
name = "snail"
vis_flags = VIS_INHERIT_PLANE
icon = 'icons/obj/service/hydroponics/equipment.dmi'
icon_state = "snail_hydrotray"
///are we currently walking?
var/is_waddling = FALSE
/obj/effect/overlay/vis_effect/snail/proc/handle_animation()
if(is_waddling)
return
is_waddling = TRUE
var/movement_direction = pixel_x >= 0 ? -1 : 1
transform = transform.Scale(-1, 1) //face the other direction
animate(src, pixel_x = movement_direction * SNAIL_MOVEMENT_DISTANCE, time = SNAIL_MOVEMENT_TIME)
addtimer(VARSET_CALLBACK(src, is_waddling, FALSE), SNAIL_MOVEMENT_TIME)
#undef SNAIL_MOVEMENT_DISTANCE
#undef SNAIL_MOVEMENT_TIME

View File

@@ -0,0 +1,104 @@
/mob/living/basic/snail
name = "snail"
desc = "Is petting this thing sanitary?"
icon_state = "snail"
icon_living = "snail"
icon_dead = "snail_dead"
base_icon_state = "snail"
held_state = "snail"
head_icon = 'icons/mob/clothing/head/pets_head.dmi'
icon = 'icons/mob/simple/pets.dmi'
butcher_results = list(/obj/item/food/meat/slab/bugmeat = 1)
mob_biotypes = MOB_ORGANIC
health = 30
maxHealth = 30
speed = 6
verb_say = "gurgles"
verb_ask = "gurgles curiously"
can_be_held = TRUE
verb_exclaim = "gurgles loudly"
verb_yell = "gurgles loudly"
worn_slot_flags = ITEM_SLOT_HEAD
faction = list(FACTION_NEUTRAL, FACTION_MAINT_CREATURES)
ai_controller = /datum/ai_controller/basic_controller/snail
/mob/living/basic/snail/Initialize(mapload)
. = ..()
var/static/list/loc_connections = list(COMSIG_ATOM_ENTERED = PROC_REF(on_entered))
AddElement(/datum/element/connect_loc, loc_connections)
var/static/list/eatable_food = list(
/obj/item/food/grown,
/obj/item/food/appleslice,
)
var/static/list/innate_actions = list(
/datum/action/cooldown/mob_cooldown/shell_retreat = BB_SNAIL_RETREAT_ABILITY,
)
grant_actions_by_list(innate_actions)
AddElement(/datum/element/ai_retaliate)
ai_controller.set_blackboard_key(BB_BASIC_FOODS, typecacheof(eatable_food))
AddElement(/datum/element/basic_eating, food_types = eatable_food)
create_reagents(100, REAGENT_HOLDER_ALIVE)
RegisterSignal(reagents, COMSIG_REAGENTS_HOLDER_UPDATED, PROC_REF(on_reagents_update))
/mob/living/basic/snail/proc/on_entered(datum/source, obj/effect/decal/cleanable/food/salt/potential_salt)
SIGNAL_HANDLER
if(istype(potential_salt))
on_salt_exposure() //immediately perish
/mob/living/basic/snail/proc/on_reagents_update(datum/source)
SIGNAL_HANDLER
if(reagents.has_reagent(/datum/reagent/consumable/salt))
on_salt_exposure()
/mob/living/basic/snail/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change)
. = ..()
if(!isturf(loc))
return
if(locate(/obj/effect/decal/cleanable/food/salt) in loc.contents)
on_salt_exposure()
/mob/living/basic/snail/proc/on_salt_exposure()
if(stat == DEAD)
return
visible_message(
span_danger("[src] shows a strong reaction after tasting some salt!"),
span_userdanger("You show a strong reaction after tasting some salt."),
)
apply_damage(500) //ouch
/mob/living/basic/snail/mob_pickup(mob/living/user)
var/obj/item/clothing/head/mob_holder/snail/holder = new(get_turf(src), src, held_state, head_icon, held_lh, held_rh, worn_slot_flags)
var/display_message = "[user] [HAS_TRAIT(src, TRAIT_MOVE_FLOATING) ? "scoops up [src]" : "peels [src] off the ground"]!"
user.visible_message(span_warning(display_message))
user.put_in_hands(holder)
/mob/living/basic/snail/update_icon_state()
if(stat != DEAD)
icon_state = HAS_TRAIT(src, TRAIT_SHELL_RETREATED) ? "[base_icon_state]_shell" : "[base_icon_state][(faction.Find(FACTION_RAT)) ? "_maints" : ""]"
return ..()
///snail's custom holder object
/obj/item/clothing/head/mob_holder/snail
/obj/item/clothing/head/mob_holder/snail/interact_with_atom(atom/interacting_with, mob/living/user, list/modifiers)
if(!istype(interacting_with, /obj/machinery/hydroponics))
return NONE
. = ITEM_INTERACT_BLOCKING
if(held_mob.stat == DEAD)
user.balloon_alert(user, "it's dead!")
return
if(locate(type) in interacting_with)
user.balloon_alert(user, "already has snail!")
return
if(!do_after(user, 2 SECONDS, interacting_with))
return
forceMove(interacting_with)
return ITEM_INTERACT_SUCCESS

View File

@@ -0,0 +1,31 @@
/datum/action/cooldown/mob_cooldown/shell_retreat
name = "Shell Retreat"
desc = "Retreat to your shell!"
cooldown_time = 5 SECONDS
click_to_activate = FALSE
button_icon = 'icons/mob/simple/pets.dmi'
button_icon_state = "snail_shell"
/datum/action/cooldown/mob_cooldown/shell_retreat/Activate(atom/target)
. = ..()
HAS_TRAIT(owner, TRAIT_SHELL_RETREATED) ? unretreat_from_shell() : retreat_into_shell()
/datum/action/cooldown/mob_cooldown/shell_retreat/proc/unretreat_from_shell()
SIGNAL_HANDLER
owner.visible_message(
span_danger("[owner] slowly pops its head out of its shell!"),
span_userdanger("You pop your head out of your shell."),
)
REMOVE_TRAIT(owner, TRAIT_SHELL_RETREATED, REF(src))
UnregisterSignal(owner, list(COMSIG_MOVABLE_ATTEMPTED_MOVE, COMSIG_LIVING_DEATH))
owner.update_appearance(UPDATE_ICON_STATE)
/datum/action/cooldown/mob_cooldown/shell_retreat/proc/retreat_into_shell()
owner.visible_message(
span_danger("[owner] quickly escapes into its shell!"),
span_userdanger("You hide in your shell.."),
)
RegisterSignals(owner, list(COMSIG_LIVING_DEATH, COMSIG_MOVABLE_ATTEMPTED_MOVE), PROC_REF(unretreat_from_shell))
ADD_TRAIT(owner, TRAIT_SHELL_RETREATED, REF(src))
owner.update_appearance(UPDATE_ICON_STATE)

View File

@@ -0,0 +1,67 @@
/datum/ai_controller/basic_controller/snail
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
)
ai_movement = /datum/ai_movement/basic_avoidance
idle_behavior = /datum/idle_behavior/idle_random_walk
planning_subtrees = list(
/datum/ai_planning_subtree/find_nearest_thing_which_attacked_me_to_flee,
/datum/ai_planning_subtree/use_mob_ability/snail_retreat,
/datum/ai_planning_subtree/find_food,
/datum/ai_planning_subtree/find_and_hunt_target/snail_people,
)
/datum/ai_planning_subtree/find_and_hunt_target/snail_people
target_key = BB_LOW_PRIORITY_HUNTING_TARGET
finding_behavior = /datum/ai_behavior/find_hunt_target/snail_people
hunting_behavior = /datum/ai_behavior/hunt_target/snail_people
hunt_targets = list(
/mob/living/carbon,
)
hunt_range = 5
hunt_chance = 45
/datum/ai_behavior/find_hunt_target/snail_people
action_cooldown = 1 MINUTES
behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
/datum/ai_behavior/find_hunt_target/snail_people/valid_dinner(mob/living/source, mob/living/carbon/potential_snail, radius, datum/ai_controller/controller, seconds_per_tick)
if(!istype(potential_snail))
return FALSE
if(potential_snail.stat != CONSCIOUS)
return FALSE
if(!is_species(potential_snail, /datum/species/snail))
return FALSE
return can_see(source, potential_snail, radius)
/datum/ai_behavior/hunt_target/snail_people
always_reset_target = TRUE
/datum/ai_behavior/hunt_target/snail_people/target_caught(mob/living/hunter, atom/hunted)
hunter.manual_emote("Celebrates around [hunted]!")
hunter.SpinAnimation(speed = 1, loops = 3)
/datum/ai_planning_subtree/use_mob_ability/snail_retreat
ability_key = BB_SNAIL_RETREAT_ABILITY
finish_planning = TRUE
/datum/ai_planning_subtree/use_mob_ability/snail_retreat/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick)
var/shell_retreated = HAS_TRAIT(controller.pawn, TRAIT_SHELL_RETREATED)
var/has_target = controller.blackboard_key_exists(BB_BASIC_MOB_CURRENT_TARGET)
if((has_target && shell_retreated) || (!has_target && !shell_retreated))
return
return ..()
/datum/ai_controller/basic_controller/snail/trash
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
BB_PET_TARGETING_STRATEGY = /datum/targeting_strategy/basic/not_friends,
)
planning_subtrees = list(
/datum/ai_planning_subtree/pet_planning,
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/basic_melee_attack_subtree,
)

View File

@@ -99,7 +99,7 @@
*/
/datum/action/cooldown/mob_cooldown/riot/proc/riot()
var/uplifted_mice = FALSE
for (var/mob/living/basic/mouse/nearby_mouse in oview(owner, range))
for(var/mob/living/basic/mouse/nearby_mouse in oview(owner, range))
uplifted_mice = convert_mouse(nearby_mouse) || uplifted_mice
if (uplifted_mice)
owner.visible_message(span_warning("[owner] commands their army to action, mutating them into rats!"))
@@ -120,6 +120,13 @@
owner.visible_message(span_warning("[owner] commands their army to action, mutating them into trash frogs!"))
return
var/uplifted_snail = FALSE
for(var/mob/living/basic/snail/nearby_snail in oview(owner, range))
uplifted_snail = convert_snail(nearby_snail, converted_check_list) || uplifted_snail
if(uplifted_snail)
owner.visible_message(span_warning("[owner] commands their army to action, mutating them into death snails!"))
return
var/rat_cap = CONFIG_GET(number/ratcap)
if (LAZYLEN(SSmobs.cheeserats) >= rat_cap)
to_chat(owner,span_warning("There's too many mice on this station to beckon a new one! Find them first!"))
@@ -210,6 +217,27 @@
make_minion(nearby_frog, crazy_frog_desc, minion_commands)
return TRUE
/datum/action/cooldown/mob_cooldown/riot/proc/convert_snail(mob/living/basic/snail/nearby_snail, list/converted_check_list)
// No need to convert when not on the same team.
if(faction_check(nearby_snail.faction, converted_check_list) || nearby_snail.stat == DEAD)
return FALSE
nearby_snail.icon_state = "[nearby_snail.base_icon_state]_maints"
nearby_snail.icon_living = "[nearby_snail.base_icon_state]_maints"
nearby_snail.icon_dead = "[nearby_snail.base_icon_state]_maints_dead"
nearby_snail.maxHealth += 10
nearby_snail.health += 10
nearby_snail.melee_damage_lower += 5
nearby_snail.melee_damage_upper += 8
nearby_snail.can_be_held = FALSE
nearby_snail.obj_damage += 8
nearby_snail.fully_replace_character_name(nearby_snail.name, "trash [nearby_snail.name]")
nearby_snail.ai_controller = new /datum/ai_controller/basic_controller/snail/trash(nearby_snail)
make_minion(nearby_snail, " ...This one doesn't look as timid.", mouse_commands)
nearby_snail.update_appearance()
return TRUE
// Command you can give to a mouse to make it kill someone
/datum/pet_command/attack/mouse
speech_commands = list("attack", "sic", "kill", "cheese em")

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.1 KiB

After

Width:  |  Height:  |  Size: 3.6 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 89 KiB

After

Width:  |  Height:  |  Size: 92 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 28 KiB

After

Width:  |  Height:  |  Size: 30 KiB

View File

@@ -4412,6 +4412,7 @@
#include "code\modules\hydroponics\plant_genes.dm"
#include "code\modules\hydroponics\seed_extractor.dm"
#include "code\modules\hydroponics\seeds.dm"
#include "code\modules\hydroponics\snail_overlay.dm"
#include "code\modules\hydroponics\unique_plant_genes.dm"
#include "code\modules\hydroponics\beekeeping\bee_smoker.dm"
#include "code\modules\hydroponics\beekeeping\beebox.dm"
@@ -5189,6 +5190,9 @@
#include "code\modules\mob\living\basic\slime\ai\controller.dm"
#include "code\modules\mob\living\basic\slime\ai\pet_command.dm"
#include "code\modules\mob\living\basic\slime\ai\subtrees.dm"
#include "code\modules\mob\living\basic\snails\snail.dm"
#include "code\modules\mob\living\basic\snails\snail_ability.dm"
#include "code\modules\mob\living\basic\snails\snail_ai.dm"
#include "code\modules\mob\living\basic\space_fauna\ant.dm"
#include "code\modules\mob\living\basic\space_fauna\cat_surgeon.dm"
#include "code\modules\mob\living\basic\space_fauna\faithless.dm"