Crashed Vulpkanin Ship - Lavaland Ruin (#32110)

* Charon's Revolver

* Dog tags

* Ash Whelps

* Ash walkers and resist adjustment

* Some providence stuff

* Minor wording adjustment

* Desc change

* Ruin, salvage, and ruin assets

* linters

* Mo linters

* quick

* Various fixes

* Variety of fixes

* Update

* Naming

* FIX
This commit is contained in:
PollardTheDragon
2026-06-24 18:37:10 -04:00
committed by GitHub
parent 9d76cfee8e
commit 4d5b0e1286
28 changed files with 5968 additions and 2 deletions
@@ -0,0 +1,167 @@
#define PW_CHALLENGE_LOOTROOM "PW_CHALLENGE_LOOTROOM"
/obj/structure/environmental_storytelling_holopad/providence
speaking_name = "Captain Charon"
/obj/structure/environmental_storytelling_holopad/providence/start_message(mob/living/carbon/human/H)
activated = TRUE
QDEL_NULL(proximity_monitor)
icon_state = "holopad1"
update_icon(UPDATE_OVERLAYS)
var/obj/effect/overlay/hologram = new(get_turf(src))
our_holo = hologram
hologram.icon = getHologramIcon(icon('icons/mob/simple_human.dmi', "captain_charon"), colour = null, opacity = 0.8, colour_blocking = TRUE) // This is more offputting. Also in colour more and less transparent.
hologram.alpha = 166
hologram.mouse_opacity = MOUSE_OPACITY_TRANSPARENT
hologram.layer = FLY_LAYER
hologram.anchored = TRUE
hologram.name = speaking_name
hologram.set_light(2)
hologram.bubble_icon = "swarmer"
hologram.pixel_y = 16
for(var/I in things_to_say)
hologram.atom_say("[I]")
sleep(loop_sleep_time)
qdel(hologram)
icon_state = initial(icon_state)
update_icon(UPDATE_OVERLAYS)
/obj/structure/environmental_storytelling_holopad/providence/bridge
things_to_say = list(
"Log. This is Captain Charon... Of the independant vessel 'Providence'...",
"Hrk... That thing... it got me good. Hah. I got it better.",
"Eckart didn't make it. They got him. I haven't heard the screams for hours now...",
"This place - it's hell. It's truly hell.",
"I'm locking down the ship. That's all I can do now.",
"If anyone heard any of my recordings, you can use the device here to disable the lockdown... but it'll probably attract the beasts to you while it works.",
"If nobody did... Hah, then why am I still talking?",
"Captain Charon, signing off... Please, before this place takes me too."
)
/obj/structure/environmental_storytelling_holopad/providence/last_stand
things_to_say = list(
"Log. This is Captain Charon of the independant vessel 'Providence...",
"The noises are growing closer. We've already seen assailants from some tribal species and other creatures.",
"They're probing our defenses - looking for a way in. We've locked down the damaged sections from what's still intact. That'll buy us time.",
"Eckart is proving to be a good skirmisher, but he's running low on shells.",
"While he buys us time, we've begun to fortify this room. We'll hold out. I promised my crew that much.",
"Captain Charon, signing off. If you see miss Protheros, tell her I love her very much."
)
/obj/structure/environmental_storytelling_holopad/providence/crew_quarters
things_to_say = list(
"Log. This is Captain Charon of the independant vessel 'Providence.",
"It's not looking good. Most of the crew were badly injured in the crash.",
"We've got what doctors are still mobile running triage, but that's not the worst of it.",
"Lieutenant Eckart took an old spacesuit out to look around, and he came back. Said he heard noises.",
"Nothing good can come of that, no sir. What crew remain alive have taken up arms. We're not going to die down here.",
"Captain Charon, signing off."
)
/obj/structure/environmental_storytelling_holopad/providence/communications
things_to_say = list(
"Log. This is Captain Charon of the independant vessel 'Providence.",
"We've sent out a distress signal. Best we can do now is wait.",
"The back half of the ship is gone. That hole in the ground... it doesn't end.",
"Sergeant Kenefick shined a targeting laser into the hole, and the rangefinder wouldn't stop ticking.",
"We've lost all contact with engineering, and a chunk of our medical deck is gone.",
"I believe we can pull through though. Things seem rough, but we've toughed through worse.",
"Captain Charon, signing off."
)
/obj/structure/providence_lockdown_controller
name = "lockdown controller"
desc = "A machine that controls the heavy blast doors all around the Providence. Reversing this should unlock the entire ship for exploration."
icon = 'icons/obj/machines/mining_machines.dmi'
icon_state = "asteroid_magnet"
anchored = TRUE
density = TRUE
armor = list(MELEE = 50, BULLET = 50, LASER = 50, ENERGY = 50, BOMB = 10, RAD = 100, FIRE = 100, ACID = 100)
resistance_flags = LAVA_PROOF | FIRE_PROOF | UNACIDABLE | ACID_PROOF
/// How long does it take to remove the lockdown
var/lockdown_time = 8 MINUTES
/// Number of waves, minus one for the starting wave
var/wave_count = 5
/// Minimum mobs per wave
var/wave_mob_min = 3
/// Max mobs per wave
var/wave_mob_max = 6
/// Mobs that can spawn in the wave
var/list/wave_mobs = list(
/mob/living/basic/mining/hivelord/legion = 375,
/mob/living/basic/ash_walker = 332,
/mob/living/basic/mining/goliath = 297,
/mob/living/basic/mining/basilisk/watcher = 296,
/mob/living/basic/ash_walker/tough = 120,
/mob/living/basic/mining/ash_whelp = 98,
/mob/living/basic/ash_walker/veteran = 45,
/mob/living/basic/mining/hivelord/legion/dwarf = 25,
/mob/living/basic/mining/goliath/ancient = 3,
/mob/living/basic/ash_walker/elite = 3,
/mob/living/basic/mining/basilisk/watcher/magmawing = 3,
/mob/living/basic/mining/ash_whelp/ice = 2,
/mob/living/basic/mining/basilisk/watcher/icewing = 1,
)
/// What faction to give the mobs that spawn
var/mob_faction = "providence_attackers"
/// Are we active
var/started = FALSE
/obj/structure/providence_lockdown_controller/attack_hand(mob/living/user)
. = ..()
if(!isliving(user))
return
if(isanimal_or_basicmob(user))
return
if(!Adjacent(user))
return
if(started)
return
start_challenge()
/obj/structure/providence_lockdown_controller/proc/start_challenge()
icon_state = "asteroid_magnet-on"
started = TRUE
atom_say("Beginning lockdown lift procedure...")
var/wave_delay = lockdown_time / wave_count
for(var/i in 1 to wave_count)
addtimer(CALLBACK(src, PROC_REF(spawn_wave)), wave_delay * i)
addtimer(CALLBACK(src, PROC_REF(finish_challenge)), lockdown_time)
sleep(5 SECONDS)
spawn_wave()
/obj/structure/providence_lockdown_controller/proc/finish_challenge()
for(var/obj/machinery/door/poddoor/P in GLOB.airlocks)
if(P.density && P.id_tag == PW_CHALLENGE_LOOTROOM && P.z == z && !P.operating)
P.open()
atom_say("Lockdown lifted. Have a secure daayayyyayyy... -ZZZT!")
visible_message("<span class='userdanger'>[src] starts beeping ominously!</span>")
for(var/i in 1 to 4)
playsound(loc, 'sound/items/timer.ogg', 30, 0)
sleep(1 SECONDS)
explosion(loc, 2, 5, 10, 16, flame_range = 20, cause = name)
qdel(src)
/obj/structure/providence_lockdown_controller/proc/spawn_wave()
var/list/valid_turfs = orange(8, get_turf(src)) - orange(3, get_turf(src))
var/num_to_spawn = rand(wave_mob_min, wave_mob_max)
for(var/i in 1 to num_to_spawn)
var/turf/valid_spawn
while(!valid_spawn)
var/turf/T = pick_n_take(valid_turfs)
if(!T.is_blocked_turf() && !T.density && !T.contains(/mob) && !istype(T, /turf/simulated/floor/pod))
valid_spawn = T
var/mob_type = pickweight(wave_mobs)
var/mob/living/new_mob = new mob_type(valid_spawn)
new /obj/effect/temp_visual/cosmic_explosion(valid_spawn)
if(!new_mob.ai_controller)
return
new_mob.ai_controller.set_blackboard_key(BB_TRAVEL_DESTINATION, get_turf(src))
new_mob.ai_controller.queue_behavior(/datum/ai_behavior/travel_towards, BB_TRAVEL_DESTINATION)
new_mob.faction |= mob_faction
/obj/effect/mob_spawn/human/corpse/vulpkanin/captain/providence
mob_name = "Charon Protheros"
mob_gender = MALE
#undef PW_CHALLENGE_LOOTROOM
+32
View File
@@ -710,6 +710,38 @@
mob_species = /datum/species/skeleton/brittle
mob_gender = NEUTER
/obj/effect/mob_spawn/human/corpse/vulpkanin
name = "vulpkanin"
mob_species = /datum/species/vulpkanin
/obj/effect/mob_spawn/human/corpse/vulpkanin/merc
outfit = /datum/outfit/vulpkanin_merc
/datum/outfit/vulpkanin_merc
name = "vulpkanin mercenary"
uniform = /obj/item/clothing/under/syndicate/tacticool
suit = /obj/item/clothing/suit/armor/vest
shoes = /obj/item/clothing/shoes/jackboots
gloves = /obj/item/clothing/gloves/color/black
/obj/effect/mob_spawn/human/corpse/vulpkanin/officer
outfit = /datum/outfit/vulpkanin_merc/officer
/datum/outfit/vulpkanin_merc/officer
name = "vulpkanin officer"
uniform = /obj/item/clothing/under/rank/procedure/blueshield
suit = /obj/item/clothing/suit/armor/secjacket
head = /obj/item/clothing/head/beret/blue
/obj/effect/mob_spawn/human/corpse/vulpkanin/captain
outfit = /datum/outfit/vulpkanin_merc/captain
/datum/outfit/vulpkanin_merc/captain
name = "vulpkanin captain"
uniform = /obj/item/clothing/under/rank/captain
suit = /obj/item/clothing/suit/armor/secjacket
head = /obj/item/clothing/head/caphat
/datum/outfit/randomizer
name = "randomizer"
@@ -0,0 +1,103 @@
/mob/living/basic/ash_walker
name = "ash walker"
desc = "These reptillian creatures appear to be related to the Unathi, but seem significantly less evolved."
icon = 'icons/mob/simple_human.dmi'
icon_state = "ashwalker"
icon_living = "ashwalker"
icon_dead = "ashwalker_dead" // Does not actually exist. del_on_death.
icon_gib = "ashwalker_gib" // Does not actually exist. del_on_death.
mob_biotypes = MOB_ORGANIC | MOB_HUMANOID
speed = -0.8
harm_intent_damage = 5
melee_damage_lower = 10
melee_damage_upper = 15
melee_attack_cooldown_min = 1 SECONDS
melee_attack_cooldown_max = 1.5 SECONDS
attack_verb_continuous = "stabs"
attack_verb_simple = "stab"
attack_sound = 'sound/weapons/bladeslice.ogg'
a_intent = INTENT_HARM
ai_controller = /datum/ai_controller/basic_controller/simple/ash_walker
faction = list("ashwalker")
weather_immunities = list("lava","ash")
unsuitable_atmos_damage = 0
minimum_survivable_temperature = 0
maximum_survivable_temperature = INFINITY
loot = list(/obj/effect/mob_spawn/human/corpse/ashwalker,
/obj/item/spear,
/obj/item/stack/ore/iron{layer = ABOVE_MOB_LAYER})
basic_mob_flags = DEL_ON_DEATH
sentience_type = SENTIENCE_OTHER
step_type = FOOTSTEP_MOB_SHOE
/mob/living/basic/ash_walker/Initialize(mapload)
. = ..()
add_language("Sinta'unathi")
set_default_language(GLOB.all_languages["Sinta'unathi"])
gender = pick(MALE, FEMALE)
AddElement(/datum/element/ai_retaliate)
/mob/living/basic/ash_walker/tough
name = "ash walker hunter"
icon_state = "ashwalker_hunter"
icon_living = "ashwalker_hunter"
damage_coeff = list(BRUTE = 0.9, BURN = 0.6, TOX = 1, CLONE = 1, STAMINA = 1, OXY = 1)
melee_damage_lower = 15
melee_damage_upper = 20
loot = list(/obj/effect/mob_spawn/human/corpse/ashwalker,
/obj/item/spear,
/obj/item/stack/ore/plasma{layer = ABOVE_MOB_LAYER})
/mob/living/basic/ash_walker/veteran
name = "ash walker slayer"
icon_state = "ashwalker_veteran"
icon_living = "ashwalker_veteran"
damage_coeff = list(BRUTE = 0.8, BURN = 0.4, TOX = 1, CLONE = 1, STAMINA = 1, OXY = 1)
armor_penetration_flat = 15
melee_damage_lower = 15
melee_damage_upper = 20
loot = list(/obj/effect/mob_spawn/human/corpse/ashwalker,
/obj/item/spear,
/obj/item/stack/ore/silver{layer = ABOVE_MOB_LAYER})
/mob/living/basic/ash_walker/elite
name = "ash walker dragonslayer"
icon_state = "ashwalker_elite"
icon_living = "ashwalker_elite"
damage_coeff = list(BRUTE = 0.5, BURN = 0, TOX = 1, CLONE = 1, STAMINA = 1, OXY = 1)
armor_penetration_flat = 25
melee_damage_lower = 20
melee_damage_upper = 25
loot = list(/obj/effect/mob_spawn/human/corpse/ashwalker,
/obj/item/spear,
/obj/item/stack/ore/gold{layer = ABOVE_MOB_LAYER})
/datum/ai_controller/basic_controller/simple/ash_walker
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
BB_TARGET_MINIMUM_STAT = UNCONSCIOUS,
)
planning_subtrees = list(
/datum/ai_planning_subtree/generic_resist,
/datum/ai_planning_subtree/random_speech/ash_walker,
/datum/ai_planning_subtree/target_retaliate,
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/attack_obstacle_in_path,
/datum/ai_planning_subtree/basic_melee_attack_subtree,
/datum/ai_planning_subtree/find_and_hunt_target/prowl/lavaland,
)
/datum/ai_planning_subtree/random_speech/ash_walker
speech_chance = 10
emote_see = list("grunts.", "hisses.", "inspects their claws.")
speak = list(
"As the necropolis wills.",
"It's been too long since we've had a good hunt.",
"Don't sleep in the shade too long. The cold dreams will find you.",
"I heard the dust singing across the rocks today.",
"The young ones think the lava fields are a brave place. They will find good hunts there.",
"Did you see that star fall?",
"Sometimes, I feel like they're watching me.",
"I heard someone got a goliath the other day. Delicious.",
"Think we'll see an offworlder today?",
"Words are cheap.")
@@ -0,0 +1,123 @@
/mob/living/basic/mining/ash_whelp
name = "ash whelp"
desc = "The offspring of an ash drake, weak in comparison but still terrifying."
icon_state = "ash_whelp"
icon_living = "ash_whelp"
icon_dead = "ash_whelp_dead"
butcher_results = list(
/obj/item/stack/ore/gold = 3,
/obj/item/stack/sheet/animalhide/ashdrake = 1,
/obj/item/stack/sheet/bone = 10,
/obj/item/stack/sheet/sinew = 2,
)
crusher_loot = list(/obj/item/crusher_trophy/tail_spike)
speed = 12
maxHealth = 300
health = 300
obj_damage = 40
armor_penetration_flat = 20
melee_damage_lower = 20
melee_damage_upper = 25
response_help_continuous = "pets"
response_help_simple = "pet"
attack_verb_continuous = "chomps"
attack_verb_simple = "chomp"
throw_blocked_message = "does nothing to the scaled hide of the"
death_message = "collapses on its side."
death_sound = 'sound/misc/demon_dies.ogg'
attack_sound = 'sound/misc/demon_attack1.ogg'
faction = list("mining")
move_force = MOVE_FORCE_STRONG
move_resist = MOVE_FORCE_STRONG
pull_force = MOVE_FORCE_STRONG
ai_controller = /datum/ai_controller/basic_controller/ash_whelp
var/list/innate_actions = list(
/datum/action/cooldown/mob_cooldown/fire_breath = BB_WHELP_STRAIGHTLINE_FIRE,
/datum/action/cooldown/mob_cooldown/fire_breath/ice/eruption/fire = BB_WHELP_WIDESPREAD_FIRE,
)
/// How much we will heal when consuming a target
var/heal_on_cannibalize = 5
/mob/living/basic/mining/ash_whelp/Initialize(mapload)
. = ..()
AddElement(/datum/element/ai_retaliate)
AddComponent(/datum/component/ai_target_timer)
AddComponent(/datum/component/footstep, FOOTSTEP_MOB_HEAVY)
grant_actions_by_list(innate_actions)
/mob/living/basic/mining/ash_whelp/early_melee_attack(atom/target, list/modifiers, ignore_cooldown)
. = ..()
if(istype(target, /obj/structure/flora/ash/rock))
create_sculpture(target)
return FALSE
if(!istype(target, type))
return TRUE
var/mob/living/victim = target
if(victim.stat != DEAD)
return TRUE
if(istype(target, /mob/living/basic/mining/ash_whelp))
cannibalize_victim(victim)
return FALSE
return FALSE
/// Carve a stone into a beautiful self-portrait
/mob/living/basic/mining/ash_whelp/proc/create_sculpture(atom/target)
visible_message("[src] starts to sculpt [target].")
if(!do_after(src, 5 SECONDS, target = target))
return
var/obj/structure/whelp_statue/dragon_statue = new(get_turf(target))
dragon_statue.name = "statue of [src]"
dragon_statue.desc = "Let this serve as a warning."
qdel(target)
/// Gib and consume our fellow ash drakes
/mob/living/basic/mining/ash_whelp/proc/cannibalize_victim(mob/living/target)
start_pulling(target)
visible_message("[src] starts to consume [target].")
if(!do_after(src, 5 SECONDS, target))
return
target.gib()
adjustBruteLoss(-1 * heal_on_cannibalize)
/// Ice whelp, the cold variant of ash whelps.
/mob/living/basic/mining/ash_whelp/ice
name = "ice whelp"
desc = "The offspring of an ash drake, weak in comparison but still terrifying. This one exudes an icy aura."
icon_state = "ice_whelp"
icon_living = "ice_whelp"
icon_dead = "ice_whelp_dead"
innate_actions = list(
/datum/action/cooldown/mob_cooldown/fire_breath/ice = BB_WHELP_STRAIGHTLINE_FIRE,
/datum/action/cooldown/mob_cooldown/fire_breath/ice/eruption = BB_WHELP_WIDESPREAD_FIRE,
)
/// Ash whelp statue formed by them carving a rock
/obj/structure/whelp_statue
name = "statue of an ash whelp"
desc = "A basalt sculpture of a proud and regal drake. Its eyes are six glowing gemstones."
icon = 'icons/obj/structures/statues.dmi'
icon_state = "drake"
density = TRUE
anchored = TRUE
integrity_failure = 100
layer = EDGED_TURF_LAYER
/obj/structure/whelp_statue/take_damage(damage_amount, damage_type = BRUTE, damage_flag = 0, sound_effect = 1, attack_dir)
. = ..()
update_icon(UPDATE_ICON_STATE)
/obj/structure/whelp_statue/update_icon_state()
if(broken)
icon_state = "drake_headless"
return
icon_state = "drake"
@@ -0,0 +1,209 @@
/datum/action/cooldown/mob_cooldown/fire_breath
name = "Fire Breath"
button_icon_state = "fireball"
desc = "Breathe a line of flames towards the target."
cooldown_time = 3 SECONDS
/// The range of the fire
var/fire_range = 15
/// The sound played when you use this ability
var/fire_sound = 'sound/magic/fireball.ogg'
/// Time to wait between spawning each fire turf
var/fire_delay = 0.15 SECONDS
/// How hot is our fire
var/fire_temperature = 1000
/// 'How much' fire do we expose the turf to?
var/fire_volume = 50
/// How much damage do you take when engulfed?
var/fire_damage = 20
/// How much damage to mechs take when engulfed?
var/mech_damage = 45
/datum/action/cooldown/mob_cooldown/fire_breath/Activate(atom/target_atom)
disable_cooldown_actions()
attack_sequence(target_atom)
StartCooldown()
enable_cooldown_actions()
return TRUE
/// Apply our specific fire breathing shape, in proc form so we can override it in subtypes
/datum/action/cooldown/mob_cooldown/fire_breath/proc/attack_sequence(atom/target)
playsound(owner.loc, fire_sound, 200, TRUE)
fire_line(target)
/// Breathe fire in a line towards the target, optionally rotated at an offset from the target
/datum/action/cooldown/mob_cooldown/fire_breath/proc/fire_line(atom/target, offset)
if(isnull(target))
return
var/turf/target_turf = get_ranged_target_turf_direct(owner, target, fire_range, offset)
var/list/turfs = get_line(owner, target_turf) - get_turf(owner)
INVOKE_ASYNC(src, PROC_REF(progressive_fire_line), turfs)
/// Creates fire with a delay on the list of targeted turfs
/datum/action/cooldown/mob_cooldown/fire_breath/proc/progressive_fire_line(list/burn_turfs)
if(QDELETED(owner) || owner.stat == DEAD)
return
// Guys we have already hit, no double dipping
var/list/hit_list = list(owner) // also don't burn ourselves
for(var/turf/target_turf in burn_turfs)
if(target_turf.is_blocked_turf(exclude_mobs = TRUE))
return
burn_turf(target_turf, hit_list, owner)
sleep(fire_delay)
/// Finally spawn the actual fire, spawns the fire hotspot in case you want to recolour it or something
/datum/action/cooldown/mob_cooldown/fire_breath/proc/burn_turf(turf/fire_turf, list/hit_list, mob/living/source)
var/obj/effect/hotspot/hotspot = new /obj/effect/hotspot/fake(fire_turf)
hotspot.temperature = fire_temperature
hotspot.recolor()
fire_turf.hotspot_expose(fire_temperature, fire_volume)
for(var/mob/living/barbecued in fire_turf.contents)
if(barbecued in hit_list)
continue
hit_list |= barbecued
on_burn_mob(barbecued, source)
for(var/obj/mecha/robotron in fire_turf.contents)
if(robotron in hit_list)
continue
hit_list |= robotron
robotron.take_damage(mech_damage, BURN, FIRE)
return
/// Do something unpleasant to someone we set on fire
/datum/action/cooldown/mob_cooldown/fire_breath/proc/on_burn_mob(mob/living/barbecued, mob/living/source)
if(fire_temperature <= TCMB)
barbecued.apply_status_effect(/datum/status_effect/ice_block_talisman, 3 SECONDS)
to_chat(barbecued, SPAN_USERDANGER("You're frozen solid by [source]'s icy breath!"))
else
to_chat(barbecued, SPAN_USERDANGER("You are burned by [source]'s fire breath!"))
barbecued.adjustFireLoss(fire_damage)
/// Shoot three lines of fire in a sort of fork pattern approximating a cone
/datum/action/cooldown/mob_cooldown/fire_breath/cone
name = "Fire Cone"
desc = "Breathe several lines of fire directed at a target."
/// The angles relative to the target that shoot lines of fire
var/list/angles = list(-40, 0, 40)
/datum/action/cooldown/mob_cooldown/fire_breath/cone/attack_sequence(atom/target)
playsound(owner.loc, fire_sound, 200, TRUE)
for(var/offset in angles)
fire_line(target, offset)
/// Shoot fire in a whole bunch of directions
/datum/action/cooldown/mob_cooldown/fire_breath/mass_fire
name = "Mass Fire"
button_icon = 'icons/effects/fire.dmi'
button_icon_state = "light"
desc = "Breathe flames in all directions."
cooldown_time = 10.5 SECONDS
click_to_activate = FALSE
/// How many fire lines do we produce to turn a full circle?
var/sectors = 12
/// How long do we wait between each spin?
var/breath_delay = 2.5 SECONDS
/// How many full circles do we perform?
var/total_spins = 3
/datum/action/cooldown/mob_cooldown/fire_breath/mass_fire/Activate(atom/target_atom)
target_atom = get_step(owner, owner.dir) // Just shoot it forwards, we don't need to click on someone forthis one
return ..()
/datum/action/cooldown/mob_cooldown/fire_breath/mass_fire/attack_sequence(atom/target)
var/queued_spins = 0
for(var/i in 1 to total_spins)
var/delay = queued_spins * breath_delay
queued_spins++
addtimer(CALLBACK(src, PROC_REF(fire_spin), target, queued_spins), delay)
/// Breathe fire in a circle, with a slight angle offset based on which of our several circles it is
/datum/action/cooldown/mob_cooldown/fire_breath/mass_fire/proc/fire_spin(target, spin_count)
if(QDELETED(owner) || owner.stat == DEAD)
return // Too dead to spin
playsound(owner.loc, fire_sound, 200, TRUE)
var/angle_increment = 360 / sectors
var/additional_offset = spin_count * angle_increment / 2
for(var/i in 1 to sectors)
fire_line(target, (angle_increment * i) + (additional_offset))
/// Breathe "fire" in a line (it's freezing cold)
/datum/action/cooldown/mob_cooldown/fire_breath/ice
name = "Ice Breath"
desc = "Fire a cold line of fire towards the enemy!"
cooldown_time = 6 SECONDS
fire_range = 7
fire_damage = 10
fire_delay = 0.85 DECISECONDS
fire_temperature = TCMB
/// Time to warn people about what we are doing
var/forecast_delay = 0.6 SECONDS
/// What turf are we aiming at?
var/turf/target_turf
/// Overlay we show when we're about to fire
var/image/forecast_overlay
/// Icon state used foroverlay
var/forecast_overlay_state = "ice_whelp_telegraph_dir"
/datum/action/cooldown/mob_cooldown/fire_breath/ice/New(Target, original)
. = ..()
forecast_overlay = image('icons/mob/lavaland/lavaland_monsters.dmi', forecast_overlay_state)
/// Apply our specific fire breathing shape, in proc form so we can override it in subtypes
/datum/action/cooldown/mob_cooldown/fire_breath/ice/attack_sequence(atom/target)
target_turf = get_turf(target)
INVOKE_ASYNC(src, PROC_REF(attack_forecast))
/// Charge up before we breathe fire
/datum/action/cooldown/mob_cooldown/fire_breath/ice/proc/attack_forecast()
owner.face_atom(target_turf)
owner.Shake(pixelshiftx = 1, pixelshifty = 0, duration = forecast_delay)
forecast_overlay.dir = (get_dir(owner, target_turf))
owner.add_overlay(forecast_overlay)
var/succeeded = do_after(owner, delay = forecast_delay, target = owner, hidden = TRUE)
owner.cut_overlay(forecast_overlay)
if(succeeded)
playsound(owner.loc, fire_sound, 200, TRUE)
breath_attack()
/// Actually breathe fire
/datum/action/cooldown/mob_cooldown/fire_breath/ice/proc/breath_attack()
owner.face_atom(target_turf)
fire_line(target_turf)
target_turf = null
/// Breathe really cold fire an area around them
/datum/action/cooldown/mob_cooldown/fire_breath/ice/eruption
name = "Ice Eruption"
desc = "Unleash cold fire in all directions"
button_icon = 'icons/effects/fire.dmi'
button_icon_state = "light"
click_to_activate = FALSE
fire_range = 3
forecast_delay = 1 SECONDS
fire_delay = 1.5 DECISECONDS
forecast_overlay_state = "ice_whelp_telegraph_all"
/datum/action/cooldown/mob_cooldown/fire_breath/ice/eruption/breath_attack()
target_turf = null
var/list/hit_list = list(owner)
var/list/nearby_turfs = list()
for(var/turf/simulated/floor/target_turf in circle_range(owner, fire_range))
if(target_turf.is_blocked_turf(exclude_mobs = TRUE))
continue
var/turf_dist = get_dist(owner, target_turf)
if(turf_dist == 0)
continue
LAZYADDASSOCLIST(nearby_turfs, "[turf_dist]", target_turf)
for(var/i in 1 to fire_range)
for(var/turf/simulated/floor/kindling as anything in nearby_turfs["[i]"])
burn_turf(kindling, hit_list, owner)
sleep(fire_delay)
///Fire subtype forash whelps
/datum/action/cooldown/mob_cooldown/fire_breath/ice/eruption/fire
name = "Eruption"
fire_temperature = 1000
@@ -0,0 +1,163 @@
/// We won't use fire breath unless we have had the same target forthis long
#define MIN_TIME_TO_BREATH_WEAPON 2 SECONDS
/datum/ai_controller/basic_controller/ash_whelp
blackboard = list(
BB_TARGETING_STRATEGY = /datum/targeting_strategy/basic,
BB_TARGET_MINIMUM_STAT = UNCONSCIOUS,
BB_AGGRO_RANGE = 5,
)
ai_movement = /datum/ai_movement/basic_avoidance
idle_behavior = /datum/idle_behavior/idle_random_walk
planning_subtrees = list(
/datum/ai_planning_subtree/generic_resist,
/datum/ai_planning_subtree/simple_find_target,
/datum/ai_planning_subtree/targeted_mob_ability/ash_whelp,
/datum/ai_planning_subtree/attack_obstacle_in_path,
/datum/ai_planning_subtree/basic_melee_attack_subtree/ash_whelp,
/datum/ai_planning_subtree/sculpt_statues,
/datum/ai_planning_subtree/find_and_hunt_target/corpses/ash_whelp,
/datum/ai_planning_subtree/burn_trees,
/datum/ai_planning_subtree/find_and_hunt_target/prowl/lavaland,
)
/// Cancel melee attacks when we have our breath weapon
/datum/ai_planning_subtree/basic_melee_attack_subtree/ash_whelp
melee_attack_behavior = /datum/ai_behavior/basic_melee_attack/ash_whelp
/// Cancel melee attacks when we have our breath weapon
/datum/ai_behavior/basic_melee_attack/ash_whelp
/datum/ai_behavior/basic_melee_attack/ash_whelp/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key)
var/datum/action/cooldown/breath_weapon = controller.blackboard[BB_TARGETED_ACTION]
if(breath_weapon?.IsAvailable())
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
return ..()
/// Find other tasty dragons
/datum/ai_planning_subtree/find_and_hunt_target/corpses/ash_whelp
target_key = BB_TARGET_CANNIBAL
finding_behavior = /datum/ai_behavior/find_hunt_target/corpses/dragon_corpse
hunting_behavior = /datum/ai_behavior/hunt_target/interact_with_target/dragon_cannibalise
hunt_targets = list(/mob/living/basic/mining/ash_whelp)
hunt_range = 10
/datum/ai_behavior/find_hunt_target/corpses/dragon_corpse
/datum/ai_behavior/find_hunt_target/corpses/dragon_corpse/valid_dinner(mob/living/source, mob/living/dinner, radius)
if(dinner.pulledby) // someone already got him before us
return FALSE
return ..()
/// Eat other dragons
/datum/ai_behavior/hunt_target/interact_with_target/dragon_cannibalise
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
/datum/ai_behavior/hunt_target/interact_with_target/dragon_cannibalise/perform(seconds_per_tick, datum/ai_controller/controller, target_key, attack_key)
var/mob/living/target = controller.blackboard[target_key]
if(QDELETED(target) || target.stat != DEAD || target.pulledby) // we were too slow
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
return ..()
/datum/ai_behavior/cannibalize/finish_action(datum/ai_controller/controller, succeeded, target_key)
. = ..()
controller.clear_blackboard_key(target_key)
/// Subtree to find icy rocks and create sculptures out of them
/datum/ai_planning_subtree/sculpt_statues
/datum/ai_planning_subtree/sculpt_statues/select_behaviors(datum/ai_controller/controller, seconds_per_tick)
if(controller.blackboard_key_exists(BB_TARGET_ROCK))
controller.queue_behavior(/datum/ai_behavior/sculpt_statue, BB_TARGET_ROCK)
return SUBTREE_RETURN_FINISH_PLANNING
controller.queue_behavior(/datum/ai_behavior/find_and_set, BB_TARGET_ROCK, /obj/structure/flora/ash/rock)
/datum/ai_behavior/sculpt_statue
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
action_cooldown = 5 MINUTES
/datum/ai_behavior/sculpt_statue/setup(datum/ai_controller/controller, target_key)
. = ..()
var/obj/target = controller.blackboard[target_key]
if(QDELETED(target))
return FALSE
set_movement_target(controller, target)
/datum/ai_behavior/sculpt_statue/perform(seconds_per_tick, datum/ai_controller/controller, target_key)
. = ..()
if(!controller.ai_interact(target = target_key, intent = INTENT_HELP))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/sculpt_statue/finish_action(datum/ai_controller/controller, succeeded, target_key)
. = ..()
controller.clear_blackboard_key(target_key)
/// Select appropriate ability based on range
/datum/ai_planning_subtree/targeted_mob_ability/ash_whelp
operational_datums = list(/datum/component/ai_target_timer)
/datum/ai_planning_subtree/targeted_mob_ability/ash_whelp/select_behaviors(datum/ai_controller/controller, seconds_per_tick)
var/mob/living/target = controller.blackboard[target_key]
if(isnull(target))
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
var/time_on_target = controller.blackboard[BB_BASIC_MOB_HAS_TARGET_TIME] || 0
if(time_on_target < MIN_TIME_TO_BREATH_WEAPON)
return // We need to spend some time acquiring our target first
var/dist_to_target = get_dist(controller.pawn, target)
var/datum/action/cooldown/mob_cooldown/fire_breath/short_range_ability = controller.blackboard[BB_WHELP_WIDESPREAD_FIRE]
if(isnull(short_range_ability) || dist_to_target > short_range_ability.fire_range)
ability_key = BB_WHELP_STRAIGHTLINE_FIRE
return ..()
ability_key = BB_WHELP_WIDESPREAD_FIRE
return ..()
/// subtree to look fortrees and burn them with our flamethrower
/datum/ai_planning_subtree/burn_trees
/datum/ai_planning_subtree/burn_trees/select_behaviors(datum/ai_controller/controller, seconds_per_tick)
var/datum/action/cooldown/using_action = controller.blackboard[BB_WHELP_STRAIGHTLINE_FIRE]
if(!using_action?.IsAvailable())
return
if(controller.blackboard_key_exists(BB_TARGET_TREE))
controller.queue_behavior(/datum/ai_behavior/targeted_mob_ability/and_clear_target/burn_trees, BB_WHELP_STRAIGHTLINE_FIRE, BB_TARGET_TREE)
return SUBTREE_RETURN_FINISH_PLANNING
controller.queue_behavior(/datum/ai_behavior/set_target_tree, BB_TARGET_TREE)
/datum/ai_behavior/set_target_tree
/datum/ai_behavior/set_target_tree/perform(seconds_per_tick, datum/ai_controller/controller, tree_key)
. = ..()
var/mob/living_pawn = controller.pawn
var/list/possible_trees = list()
for(var/obj/structure/flora/tree/possible_tree in oview(9, living_pawn))
possible_trees += possible_tree
if(!length(possible_trees))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_FAILED
controller.set_blackboard_key(tree_key, pick(possible_trees))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/targeted_mob_ability/and_clear_target/burn_trees
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
required_distance = 2
action_cooldown = 2 MINUTES
/datum/ai_behavior/targeted_mob_ability/and_clear_target/burn_trees/setup(datum/ai_controller/controller, ability_key, target_key)
. = ..()
var/obj/target = controller.blackboard[target_key]
if(QDELETED(target))
return FALSE
set_movement_target(controller, target)
#undef MIN_TIME_TO_BREATH_WEAPON
@@ -14,6 +14,14 @@
multi_sprite_step = 1
icon_state = "357_box"
/obj/item/ammo_box/huntsman32
name = "speed loader (.32 Huntsman)"
desc = "A small device designed to quickly reload revolvers. Seven round capacity."
materials = list()
ammo_type = /obj/item/ammo_casing/huntsman32
multi_sprite_step = 1 // see: /obj/item/ammo_box/update_icon()
icon_state = "357"
/obj/item/ammo_box/c9mm
name = "ammo box (9mm)"
desc = "An ammunition box filled with 9mm pistol cartridges, commonly used in handguns and submachine guns."
@@ -7,6 +7,15 @@
muzzle_flash_strength = MUZZLE_FLASH_STRENGTH_NORMAL
muzzle_flash_range = MUZZLE_FLASH_RANGE_STRONG
/obj/item/ammo_casing/huntsman32
name = ".32 huntsman round"
desc = "A .32 caliber round. The bullet is scored and designed to fragment on hit, dealing large amounts of damage to large creatures."
icon_state = "magnum_steel"
caliber = "32"
projectile_type = /obj/projectile/bullet
muzzle_flash_strength = MUZZLE_FLASH_STRENGTH_NORMAL
muzzle_flash_range = MUZZLE_FLASH_RANGE_STRONG
/obj/item/ammo_casing/rubber9mm
name = "9mm rubber round"
desc = "A 9mm pistol cartridge. This one has a rubber tip for less-lethal takedowns."
@@ -19,7 +28,7 @@
desc = "A 7.62mm rifle cartridge, often used in Soviet rifles."
icon_state = "rifle_brass"
caliber = "a762"
projectile_type = /obj/projectile/bullet
projectile_type = /obj/projectile/bullet/huntsman32
muzzle_flash_strength = MUZZLE_FLASH_STRENGTH_STRONG
muzzle_flash_range = MUZZLE_FLASH_RANGE_STRONG
@@ -57,6 +57,10 @@
return 0
/obj/item/ammo_box/magazine/internal/cylinder/charons_special
ammo_type = /obj/item/ammo_casing/huntsman32
caliber = "32"
/obj/item/ammo_box/magazine/internal/cylinder/rev38/invisible
name = "finger gun cylinder"
desc = "Wait, what?"
@@ -132,6 +132,14 @@
options["Fancy"] = "revolver-fancy"
options["Rose Gold Finish"] = "revolver-rose"
/// Special revolver found in the vulpkanin ship lavaland ruin
/obj/item/gun/projectile/revolver/charons_special
name = "Charon's Modified Revolver"
desc = "A bog-standard service revolver, modified to fire special .32 caliber \"Huntsman\" rounds. The barrel is engraved with a captain's insignia and a pair of wings."
icon_state = "charons-special"
mag_type = /obj/item/ammo_box/magazine/internal/cylinder/charons_special
origin_tech = "combat=6;materials=5"
/// Summoned by the Finger Gun spell, from advanced mimery traitor item
/obj/item/gun/projectile/revolver/fingergun
name = "finger gun"
@@ -52,6 +52,15 @@
stamina = 30
icon_state = "bullet-r"
/obj/projectile/bullet/huntsman32
damage = 20
/obj/projectile/bullet/huntsman32/on_hit(atom/target, blocked = 0)
if(..(target, blocked) && isliving(target))
var/mob/living/L = target
if(L.mob_size >= MOB_SIZE_LARGE)
L.apply_damage(80, BRUTE)
/obj/projectile/bullet/toxinbullet
damage = 15
damage_type = TOX