Basic diona nymphs, spiderbots, and crabs (#30377)

* Basic nymphs, spiderbots, and crabs

* Nutrition fix

* Forgot a line

* Cooldown on eat

---------

Signed-off-by: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com>
This commit is contained in:
PollardTheDragon
2025-10-01 14:32:00 +00:00
committed by GitHub
parent ad98a804cb
commit e14a43eea1
36 changed files with 281 additions and 183 deletions
@@ -0,0 +1,73 @@
//Look Sir, free crabs!
/mob/living/basic/crab
name = "crab"
desc = "A hard-shelled crustacean. Seems quite content to lounge around all the time."
icon_state = "crab"
icon_living = "crab"
icon_dead = "crab_dead"
speak_emote = list("clicks")
butcher_results = list(/obj/item/food/meat = 1)
response_help_continuous = "pets"
response_help_simple = "pet"
response_disarm_continuous = "pushes"
response_disarm_simple = "push"
response_harm_continuous = "stomps"
response_harm_simple = "stomp"
friendly_verb_continuous = "pinches"
friendly_verb_simple = "pinch"
ventcrawler = VENTCRAWLER_ALWAYS
can_hide = TRUE
gold_core_spawnable = FRIENDLY_SPAWN
ai_controller = /datum/ai_controller/basic_controller/crab
/mob/living/basic/crab/Initialize(mapload)
. = ..()
AddElement(/datum/element/wears_collar)
AddElement(/datum/element/ai_retaliate)
AddElement(/datum/element/sideways_movement)
/mob/living/basic/crab/valid_respawn_target_for(mob/user)
. = ..()
return TRUE
// COFFEE! SQUEEEEEEEEE!
/mob/living/basic/crab/coffee
name = "Coffee"
real_name = "Coffee"
desc = "It's Coffee, the other pet!"
gold_core_spawnable = NO_SPAWN
/mob/living/basic/crab/evil
name = "Evil Crab"
real_name = "Evil Crab"
desc = "Unnerving, isn't it? It has to be planning something nefarious..."
icon_state = "evilcrab"
icon_living = "evilcrab"
icon_dead = "evilcrab_dead"
response_help_continuous = "pokes"
response_help_simple = "poke"
response_disarm_continuous = "shoves"
response_disarm_simple = "shove"
gold_core_spawnable = HOSTILE_SPAWN
/// Crabs don't do much, but they want to live. They will flee when attacked, but will opportunistically melee attack people who do
/datum/ai_controller/basic_controller/crab
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
)
ai_traits = AI_FLAG_STOP_MOVING_WHEN_PULLED | AI_FLAG_PAUSE_DURING_DO_AFTER
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/flee_target,
/datum/ai_planning_subtree/basic_melee_attack_subtree/opportunistic,
/datum/ai_planning_subtree/random_speech/crab,
)
/datum/ai_planning_subtree/random_speech/crab
speech_chance = 5
emote_hear = list("clicks")
emote_see = list("clacks")
@@ -0,0 +1,343 @@
/*
* Tiny babby plant critter plus procs.
*/
//Mob defines.
#define GESTALT_ALERT "gestalt screen alert"
#define NYMPH_ALERT "nymph screen alert"
/mob/living/basic/diona_nymph
name = "diona nymph"
icon = 'icons/mob/monkey.dmi'
icon_state = "nymph"
icon_living = "nymph"
icon_dead = "nymph_dead"
icon_resting = "nymph_sleep"
pass_flags = PASSTABLE | PASSMOB
mob_biotypes = MOB_ORGANIC | MOB_PLANT
mob_size = MOB_SIZE_SMALL
ventcrawler = VENTCRAWLER_ALWAYS
atmos_requirements = list("min_oxy" = 0, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0)
minimum_survivable_temperature = 0
maxHealth = 50
health = 50
voice_name = "diona nymph"
speak_emote = list("chirrups")
response_help_continuous = "pets"
response_help_simple = "pet"
response_disarm_continuous = "pushes"
response_disarm_simple = "push"
response_harm_continuous = "kicks"
response_harm_simple = "kick"
melee_damage_lower = 5
melee_damage_upper = 8
melee_attack_cooldown_min = 1.5 SECONDS
melee_attack_cooldown_max = 2.5 SECONDS
attack_verb_continuous = "bites"
attack_verb_simple = "bite"
attack_sound = 'sound/weapons/bite.ogg'
ai_controller = /datum/ai_controller/basic_controller/diona_nymph
var/list/donors = list()
holder_type = /obj/item/holder/diona
/// Amount of blood donors needed to evolve
var/evolve_donors = 5
/// Amount of blood donors needed for understand language
var/awareness_donors = 3
/// Amount of nutrition needed before evolving
var/nutrition_need = 500
/// List of stuff (mice) that we want to eat
var/static/list/edibles = list(
/obj/item/food/grown,
)
var/datum/action/innate/diona_nymph/merge/merge_action = new()
var/datum/action/innate/diona_nymph/evolve/evolve_action = new()
var/datum/action/innate/diona_nymph/steal_blood/steal_blood_action = new()
/mob/living/basic/diona_nymph/Initialize(mapload)
. = ..()
AddElement(/datum/element/wears_collar)
if(name == initial(name)) // To stop named dionas becoming generic.
name = "[name] ([rand(1, 1000)])"
real_name = name
add_language("Rootspeak")
merge_action.Grant(src)
evolve_action.Grant(src)
steal_blood_action.Grant(src)
AddElement(/datum/element/ai_retaliate)
AddElement(/datum/element/basic_eating, heal_amt_ = 2, food_types_ = edibles)
RegisterSignal(src, COMSIG_MOB_PRE_EAT, PROC_REF(check_full))
RegisterSignal(src, COMSIG_MOB_ATE, PROC_REF(consume))
ai_controller.set_blackboard_key(BB_BASIC_FOODS, typecacheof(edibles))
/datum/action/innate/diona_nymph/merge
name = "Merge with gestalt"
button_icon = 'icons/mob/human_races/r_diona.dmi'
button_icon_state = "preview"
/datum/action/innate/diona_nymph/merge/Activate()
var/mob/living/basic/diona_nymph/user = owner
user.merge()
/datum/action/innate/diona_nymph/evolve
name = "Evolve"
button_icon = 'icons/obj/cloning.dmi'
button_icon_state = "pod_cloning"
/datum/action/innate/diona_nymph/evolve/Activate()
var/mob/living/basic/diona_nymph/user = owner
user.evolve()
/datum/action/innate/diona_nymph/steal_blood
name = "Steal blood"
button_icon = 'icons/goonstation/objects/iv.dmi'
button_icon_state = "bloodbag"
/datum/action/innate/diona_nymph/steal_blood/Activate()
var/mob/living/basic/diona_nymph/user = owner
user.steal_blood()
/mob/living/basic/diona_nymph/UnarmedAttack(atom/A)
// Can't attack your gestalt
if(isdiona(A) && (src in A.contents))
visible_message("[src] wiggles around a bit.")
else
..()
/mob/living/basic/diona_nymph/run_resist()
if(!split())
..()
/mob/living/basic/diona_nymph/attack_hand(mob/living/carbon/human/M)
// Let people pick the little buggers up.
if(M.a_intent == INTENT_HELP)
if(isdiona(M))
to_chat(M, "You feel your being twine with that of [src] as it merges with your biomass.")
to_chat(src, "You feel your being twine with that of [M] as you merge with its biomass.")
// adds a screen alert that can call resist
throw_alert(GESTALT_ALERT, /atom/movable/screen/alert/nymph, new_master = src)
M.throw_alert(NYMPH_ALERT, /atom/movable/screen/alert/gestalt, new_master = src)
forceMove(M)
else if(isrobot(M))
M.visible_message("<span class='notice'>[M] playfully boops [src] on the head!</span>", "<span class='notice'>You playfully boop [src] on the head!</span>")
else
get_scooped(M)
else
..()
/mob/living/basic/diona_nymph/proc/merge()
if(stat != CONSCIOUS)
return FALSE
var/list/choices = list()
for(var/mob/living/carbon/human/H in view(1,src))
if(!(Adjacent(H)) || !isdiona(H))
continue
choices += H
if(!length(choices))
to_chat(src, "<span class='warning'>No suitable diona nearby.</span>")
return FALSE
var/mob/living/M = tgui_input_list(src, "Who do you wish to merge with?", "Nymph Merging", choices)
// input can take a while, so re-validate
if(!M || !src || !(Adjacent(M)) || stat != CONSCIOUS)
return FALSE
if(isdiona(M))
to_chat(M, "You feel your being twine with that of [src] as it merges with your biomass.")
M.status_flags |= PASSEMOTES
to_chat(src, "You feel your being twine with that of [M] as you merge with its biomass.")
forceMove(M)
// adds a screen alert that can call resist
throw_alert(GESTALT_ALERT, /atom/movable/screen/alert/nymph, new_master = src)
M.throw_alert(NYMPH_ALERT, /atom/movable/screen/alert/gestalt, new_master = src)
return TRUE
else
return FALSE
/mob/living/basic/diona_nymph/proc/split(forced = FALSE)
if((stat != CONSCIOUS && !forced) || !isdiona(loc))
return FALSE
var/mob/living/carbon/human/D = loc
var/turf/T = get_turf(src)
if(!T)
return FALSE
to_chat(loc, "You feel a pang of loss as [src] splits away from your biomass.")
to_chat(src, "You wiggle out of the depths of [loc]'s biomass and plop to the ground.")
forceMove(T)
var/hasMobs = FALSE
for(var/atom/A in D.contents)
if(ismob(A) || istype(A, /obj/item/holder))
hasMobs = TRUE
if(!hasMobs)
D.status_flags &= ~PASSEMOTES
D.clear_alert(NYMPH_ALERT)
clear_alert(GESTALT_ALERT)
return TRUE
/mob/living/basic/diona_nymph/proc/evolve()
if(stat != CONSCIOUS)
return FALSE
if(length(donors) < evolve_donors)
to_chat(src, "<span class='warning'>You need more blood in order to ascend to a new state of consciousness...</span>")
return FALSE
if(nutrition < nutrition_need)
to_chat(src, "<span class='warning'>You need to binge on weeds in order to have the energy to grow...</span>")
return FALSE
if(isdiona(loc) && !split()) // if it's merged with diona, needs to able to split before evolving
return FALSE
visible_message("<span class='danger'>[src] begins to shift and quiver, and erupts in a shower of shed bark as it splits into a tangle of nearly a dozen new dionaea.</span>","<span class='danger'>You begin to shift and quiver, feeling your awareness splinter. All at once, we consume our stored nutrients to surge with growth, splitting into a tangle of at least a dozen new dionaea. We have attained our gestalt form.</span>")
if(master_commander)
to_chat(src, "<span class='userdanger'>As you evolve, your mind grows out of it's restraints. You are no longer loyal to [master_commander]!</span>")
var/mob/living/carbon/human/diona/adult = new(get_turf(loc))
adult.set_species(/datum/species/diona)
if(istype(loc, /obj/item/holder/diona))
var/obj/item/holder/diona/L = loc
forceMove(L.loc)
qdel(L)
for(var/datum/language/L in languages)
adult.add_language(L.name)
adult.regenerate_icons()
adult.name = "diona ([rand(100,999)])"
adult.real_name = adult.name
adult.ckey = ckey
adult.real_name = adult.dna.species.get_random_name() // I hate this being here of all places but unfortunately dna is based on real_name!
// [Nymph -> Diona] is from xenobio (or botany) and does not give vampires usuble blood and cannot be converted by cult.
ADD_TRAIT(adult.mind, TRAIT_XENOBIO_SPAWNED_HUMAN, ROUNDSTART_TRAIT)
for(var/obj/item/W in contents)
drop_item_to_ground(W)
qdel(src)
return TRUE
/mob/living/basic/diona_nymph/proc/check_full(datum/source, obj/item/potential_food)
SIGNAL_HANDLER // COMSIG_MOB_PRE_EAT
if(nutrition >= nutrition_need) // Prevents griefing by overeating food items without evolving.
to_chat(src, "<span class='warning'>You're too full to consume this! Perhaps it's time to grow bigger...</span>")
return COMSIG_MOB_TERMINATE_EAT
// Consumes plant matter other than weeds to evolve
/mob/living/basic/diona_nymph/proc/consume(datum/source, obj/item/potential_food)
SIGNAL_HANDLER // COMSIG_MOB_ATE
if(potential_food.reagents.get_reagent_amount("nutriment") + potential_food.reagents.get_reagent_amount("plantmatter") < 1)
adjust_nutrition(2)
else
adjust_nutrition((potential_food.reagents.get_reagent_amount("nutriment") + potential_food.reagents.get_reagent_amount("plantmatter")) * 2)
/mob/living/basic/diona_nymph/proc/steal_blood()
if(stat != CONSCIOUS)
return FALSE
var/list/choices = list()
for(var/mob/living/carbon/human/H in oview(1,src))
if(Adjacent(H) && H.dna && !(NO_BLOOD in H.dna.species.species_traits))
choices += H
if(!length(choices))
to_chat(src, "<span class='warning'>No suitable blood donors nearby.</span>")
return FALSE
var/mob/living/carbon/human/M = tgui_input_list(src, "Who do you wish to take a sample from?", "Blood Sampling", choices)
if(!M || !src || !(Adjacent(M)) || stat != CONSCIOUS) //input can take a while, so re-validate
return FALSE
if(!M.dna || (NO_BLOOD in M.dna.species.species_traits))
to_chat(src, "<span class='warning'>That donor has no blood to take.</span>")
return FALSE
if(donors.Find(M.real_name))
to_chat(src, "<span class='warning'>That donor offers you nothing new.</span>")
return FALSE
visible_message("<span class='danger'>[src] flicks out a feeler and neatly steals a sample of [M]'s blood.</span>","<span class='danger'>You flick out a feeler and neatly steal a sample of [M]'s blood.</span>")
donors += M.real_name
for(var/datum/language/L in M.languages)
if(!(L.flags & HIVEMIND))
languages |= L
spawn(25)
update_progression()
/mob/living/basic/diona_nymph/proc/update_progression()
if(stat != CONSCIOUS || !length(donors))
return FALSE
if(length(donors) == evolve_donors)
to_chat(src, "<span class='noticealien'>You feel ready to move on to your next stage of growth.</span>")
else if(length(donors) == awareness_donors)
universal_understand = TRUE
to_chat(src, "<span class='noticealien'>You feel your awareness expand, and realize you know how to understand the creatures around you.</span>")
else
to_chat(src, "<span class='noticealien'>The blood seeps into your small form, and you draw out the echoes of memories and personality from it, working them into your budding mind.</span>")
/mob/living/basic/diona_nymph/put_in_hands(obj/item/W)
W.forceMove(get_turf(src))
W.layer = initial(W.layer)
W.plane = initial(W.plane)
W.dropped()
/mob/living/basic/diona_nymph/put_in_active_hand(obj/item/W)
to_chat(src, "<span class='warning'>You don't have any hands!</span>")
return
/mob/living/basic/diona_nymph/valid_respawn_target_for(mob/user)
if(!jobban_isbanned(user, ROLE_NYMPH))
return TRUE
return FALSE
/// Diona nymphs want to eat plant matter to get to evolution stage, but won't evolve or sample blood on their own. They flee when attacked.
/datum/ai_controller/basic_controller/diona_nymph
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
)
ai_traits = AI_FLAG_STOP_MOVING_WHEN_PULLED | AI_FLAG_PAUSE_DURING_DO_AFTER
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/flee_target,
/datum/ai_planning_subtree/find_food/diona_nymph,
/datum/ai_planning_subtree/random_speech/diona_nymph,
)
/datum/ai_planning_subtree/find_food/diona_nymph
/datum/ai_planning_subtree/find_food/diona_nymph/select_behaviors(datum/ai_controller/controller, seconds_per_tick)
var/mob/living/basic/diona_nymph/nymph = controller.pawn
if(nymph.nutrition >= nymph.nutrition_need)
// Put eating on cooldown so it doesn't keep trying to eat.
var/food_cooldown = controller.blackboard[BB_EAT_FOOD_COOLDOWN] || EAT_FOOD_COOLDOWN
controller.set_blackboard_key(BB_NEXT_FOOD_EAT, world.time + food_cooldown)
return SUBTREE_RETURN_FINISH_PLANNING
return ..()
/datum/ai_planning_subtree/random_speech/diona_nymph
speech_chance = 5
emote_hear = list("chirrups.")
emote_see = list("chirrups.")
#undef GESTALT_ALERT
#undef NYMPH_ALERT
@@ -0,0 +1,184 @@
// No AI controller for these guys - they should be inert if they're not player controlled.
/mob/living/basic/spiderbot
name = "Spider-bot"
desc = "A skittering robotic friend!" // More like ultimate shitter
icon = 'icons/mob/robots.dmi'
icon_state = "spiderbot-chassis"
icon_living = "spiderbot-chassis"
icon_dead = "spiderbot-smashed"
universal_speak = TRUE
health = 40
maxHealth = 40
pass_flags = PASSTABLE
melee_damage_lower = 2
melee_damage_upper = 2
melee_damage_type = BURN
attack_verb_continuous = "shocks"
attack_verb_simple = "shocks"
attack_sound = "sparks"
response_help_continuous = "pets"
response_help_simple = "pet"
response_disarm_continuous = "shoos"
response_disarm_simple = "shoo"
response_harm_continuous = "stomps on"
response_harm_simple = "stomps on"
speed = 0
mob_biotypes = MOB_ROBOTIC
mob_size = MOB_SIZE_SMALL
speak_emote = list("beeps", "clicks", "chirps")
atmos_requirements = list("min_oxy" = 0, "max_oxy" = 0, "min_tox" = 0, "max_tox" = 0, "min_co2" = 0, "max_co2" = 0, "min_n2" = 0, "max_n2" = 0)
minimum_survivable_temperature = 0
maximum_survivable_temperature = 500
can_hide = TRUE
ventcrawler = VENTCRAWLER_ALWAYS
loot = list(/obj/effect/decal/cleanable/blood/gibs/robot)
basic_mob_flags = DEL_ON_DEATH
/// Is it getting ready to explode?
var/emagged = FALSE
/// MMI it contains
var/obj/item/mmi/mmi = null
/// Who emagged the spiderbot
var/mob/emagged_master = null
/mob/living/basic/spiderbot/Destroy()
if(emagged)
QDEL_NULL(mmi)
explosion(get_turf(src), -1, -1, 3, 5, cause = "Emagged spiderbot destruction")
else
eject_brain()
return ..()
/mob/living/basic/spiderbot/item_interaction(mob/living/user, obj/item/O, list/modifiers)
if(istype(O, /obj/item/mmi))
var/obj/item/mmi/B = O
if(mmi) // There's already a brain in it.
to_chat(user, "<span class='warning'>There's already a brain in [src]!</span>")
return ITEM_INTERACT_COMPLETE
if(!B.brainmob)
to_chat(user, "<span class='warning'>Sticking an empty MMI into the frame would sort of defeat the purpose.</span>")
return ITEM_INTERACT_COMPLETE
if(!B.brainmob.key)
var/ghost_can_reenter = 0
if(B.brainmob.mind)
for(var/mob/dead/observer/ghost in GLOB.player_list)
if(ghost.ghost_flags & GHOST_CAN_REENTER && ghost.mind == B.brainmob.mind)
ghost_can_reenter = 1
break
for(var/mob/living/basic/S in GLOB.player_list)
if(HAS_TRAIT(S, TRAIT_RESPAWNABLE))
ghost_can_reenter = 1
break
if(!ghost_can_reenter)
to_chat(user, "<span class='notice'>[B] is completely unresponsive; there's no point.</span>")
return ITEM_INTERACT_COMPLETE
if(B.brainmob.stat == DEAD)
to_chat(user, "<span class='warning'>[B] is dead. Sticking it into the frame would sort of defeat the purpose.</span>")
return ITEM_INTERACT_COMPLETE
if(jobban_isbanned(B.brainmob, "Cyborg") || jobban_isbanned(B.brainmob, "nonhumandept"))
to_chat(user, "<span class='warning'>[B] does not seem to fit.</span>")
return ITEM_INTERACT_COMPLETE
to_chat(user, "<span class='notice'>You install [B] in [src]!</span>")
user.drop_item()
B.forceMove(src)
mmi = B
transfer_personality(B)
update_icon()
return ITEM_INTERACT_COMPLETE
else if(istype(O, /obj/item/card/id) || istype(O, /obj/item/pda))
if(!mmi)
to_chat(user, "<span class='warning'>There's no reason to swipe your ID - the spiderbot has no brain to remove.</span>")
return ITEM_INTERACT_COMPLETE
if(emagged)
to_chat(user, "<span class='warning'>[src] doesn't seem to respond.</span>")
return ITEM_INTERACT_COMPLETE
var/obj/item/card/id/id_card
if(istype(O, /obj/item/card/id))
id_card = O
else
var/obj/item/pda/pda = O
id_card = pda.id
if(ACCESS_ROBOTICS in id_card.access)
to_chat(user, "<span class='notice'>You swipe your access card and pop the brain out of [src].</span>")
eject_brain()
return ITEM_INTERACT_COMPLETE
else
to_chat(user, "<span class='warning'>You swipe your card, with no effect.</span>")
return ITEM_INTERACT_COMPLETE
/mob/living/basic/spiderbot/welder_act(mob/user, obj/item/I)
if(user.a_intent != INTENT_HELP)
return
if(user == src) // No self-repair dummy
return
if(health >= maxHealth)
to_chat(user, "<span class='warning'>[src] does not need repairing!</span>")
return
. = TRUE
if(!I.use_tool(src, user, volume = I.tool_volume))
return
adjustHealth(-5)
add_fingerprint(user)
user.visible_message("[user] repairs [src]!","<span class='notice'>You repair [src].</span>")
/mob/living/basic/spiderbot/emag_act(mob/living/user)
if(emagged)
to_chat(user, "<span class='warning'>[src] doesn't seem to respond.</span>")
return 0
else
emagged = TRUE
to_chat(user, "<span class='notice'>You short out the security protocols and rewrite [src]'s internal memory.</span>")
to_chat(src, "<span class='userdanger'>You have been emagged; you are now completely loyal to [user] and [user.p_their()] every order!</span>")
emagged_master = user
add_attack_logs(user, src, "Emagged")
maxHealth = 60
health = 60
melee_damage_lower = 15
melee_damage_upper = 15
attack_sound = 'sound/machines/defib_zap.ogg'
return TRUE
/mob/living/basic/spiderbot/proc/transfer_personality(obj/item/mmi/M)
mind = M.brainmob.mind
mind.key = M.brainmob.key
ckey = M.brainmob.ckey
name = "Spider-bot ([M.brainmob.name])"
if(emagged)
to_chat(src, "<span class='userdanger'>You have been emagged; you are now completely loyal to [emagged_master] and [emagged_master.p_their()] every order!</span>")
/mob/living/basic/spiderbot/update_icon_state()
if(mmi)
if(istype(mmi, /obj/item/mmi))
icon_state = "spiderbot-chassis-mmi"
icon_living = "spiderbot-chassis-mmi"
if(istype(mmi, /obj/item/mmi/robotic_brain))
icon_state = "spiderbot-chassis-posi"
icon_living = "spiderbot-chassis-posi"
else
icon_state = "spiderbot-chassis"
icon_living = "spiderbot-chassis"
/mob/living/basic/spiderbot/proc/eject_brain()
if(mmi)
var/turf/T = get_turf(src)
mmi.forceMove(T)
if(mind)
mind.transfer_to(mmi.brainmob)
mmi = null
name = "Spider-bot"
update_icon()