The Divine Flock - DaedalusDock Flockmind Port (#31984)

* Initial commit - FLOCKMIND - Probably has like a billion things to fix

* Do after conversions

* Config

* Moved the files, icon fixes

* Tick everything, language work, event, spawn landmark, role prefs, beginning mob port

* Spans and some other fixes. Also the tickening

* More tickening

* More fixes. Lots of fixes.

* More Fixes

* A whole lot more. Also flock TGUI.

* Fixes fixes fixes fixes fixes

* FIXES

* More fixes - PR ready, still needs a fuckton of testing

* Fixes

* fix incomplete upstream merge

* fix FlockPanel + sort button name

* TGUI review

* Fixes tealprint list

* Fixes

* More fixes

* Incapacitator Fix

* Filenames

* Linters

* Interceptor range buff

* Reagent counts

* Linters

* Fabricator vendor fix

* Keybinds and HUD - Flockdrones, Fixes Vendor Conversion, Cube Materials

* Reworks reagent, adds flock grilles, fixes compute node overlays

* Intent-based flockdrone parts

* Intent based drone parts

* Radial control panel for controlling drones manually, phasing through windows/grilles

* Movement fixes

* Radio talk power, stare fix

* Flock health HUD

* Fixes flock lights, linters

* Unit tests

* Adds countdown to relay

* Relay improvements

* Small fix

* Logic Schmogic

* Relay overlay and looping sound effect

* Ignore air when converting turfs

* Cage fixes and improvements

* Improved flock bolt

* Turret conversions

* Flock bolts taze simple or basic mobs

* Sentience type

* Fixe

* Linter

* tgui review stage 2

* Concentrated Repair Burst

* Improves radio detection

* Removes extra space

* Adds healing visual effect

* Cube tech levels

* Ghooost

* Excess

* Flock doors, chairs, lattices. Centralizes conversion code.  Crafting with Gnesis

* Update code/modules/antagonists/flockmind/ai_behaviors/flock_wander.dm

Co-authored-by: Kapu1178 <75460809+Kapu1178@users.noreply.github.com>
Signed-off-by: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com>

* Fixes the fix

* Astar movement detection

* Fix, extraneous code, language stuff

* Language fixes and wander fix

* Fixes

* Another fix

* Lints

* Another linter

* Language improvement

* More language improvements

* Time requirement and appearing in orbit menu as an antag

* Cube glow

* TGUI

* Minicache

* Linters

* Grammar

* Material ID fix

* Lid fix

* Reagent turf reaction

* Reagent fix

* Butcher results

* Conversion rates

* Flock stare fix

* Fixes stare behavior

* Staring

* Flock mob blood

* Flock mobs gibs and blood. Also some runtime fixes

* Flock mobs now resist out of grabs, buckles, lockers, and more

* Fixes flock orbit, fixes a runtime I think,

* Target mechs, damage mechs, other bug fixes

* Cage fix

* Cage resist change

* Some mind changes, gatecrash buff

* Drones now shoot mechs, stare improvement

* Cut down on spam a little

* Nest fix

* No more resist spam

* Fixed drone death control

* Resist statement

* Makes the relay alarm scarier

* Fixes dead flock camera mobs having no ghost sprite, something with ghosting

* Enhanced flockphasing

* Improved flockmob pathing

* Added required turf restriction to relay

* Increased needed bandwidth for relay construction

* Nerfed drone substrate rate

* Added new status tab items for relay progress

* Another relay cost adjustment

* Improves drone AI responsiveness

* Computer frames now become flock computers

* Improves target finding for conversion, building, and replicating

* Reduced flock event pop requirements

* Adjusts flock protection on structures. Adjusts overlays.

* Relay unlock tweak

* Fixwes flock being able to gib mech'd AIs with one button

* Map conflict

* Flock can no longer be outed by merely existing

* Fied bug causing drones to shoot themselves

* Prevents mobs from attacking while in a cage

* Converter tool can now open closets and crates.

* Adds descriptions to flockdrone tools.

* More informatic blurbs

* Adds xenobiology organs

* Organ lint

* TGUI merge

* bundle and mm

---------

Signed-off-by: PollardTheDragon <144391971+PollardTheDragon@users.noreply.github.com>
Co-authored-by: Toastical <20125180+Toastical@users.noreply.github.com>
Co-authored-by: Kapu1178 <75460809+Kapu1178@users.noreply.github.com>
Co-authored-by: Burzah <116982774+Burzah@users.noreply.github.com>
This commit is contained in:
PollardTheDragon
2026-07-08 17:31:28 -04:00
committed by GitHub
parent ab05a4ee3a
commit 24845b238a
228 changed files with 9724 additions and 49 deletions
@@ -0,0 +1,42 @@
/datum/action/cooldown/flock/cage_mob
name = "Cage"
cooldown_time = 5 SECONDS
click_to_activate = TRUE
/datum/action/cooldown/flock/cage_mob/is_valid_target(atom/cast_on)
return isliving(cast_on) && !isflockmob(cast_on) && isturf(cast_on.loc)
/datum/action/cooldown/flock/cage_mob/Activate(atom/target)
if(DOING_INTERACTION(owner, "flock_cage"))
return FALSE
var/mob/living/basic/flock/drone/bird = owner
bird.stop_flockphase(TRUE)
var/turf/T = get_turf(target)
owner.visible_message(
SPAN_NOTICE("<b>[owner]</b> begins forming a cuboid structure around <b>[target]</b>."),
blind_message = SPAN_HEAR("You hear a strange synthetic whirring."),
)
var/obj/effect/temp_visual/flock_effect
if(iswallturf(T))
flock_effect = new /obj/effect/temp_visual/flock/wall_convert(T)
log_attack(owner, target, "attempted to cage")
. = TRUE
playsound(owner, 'sound/goonstation/flockmind/flockdrone_build.ogg', 30, TRUE, extrarange = SILENCED_SOUND_EXTRARANGE)
if(!do_after(owner, 4.5 SECONDS, target = target, interaction_key = "flock_cage"))
. = FALSE
qdel(flock_effect)
if(!.)
return
log_attack(owner, target, "caged")
playsound(owner, 'sound/goonstation/flockmind/flockdrone_build_complete.ogg', 70, TRUE)
var/obj/structure/flock/cage/cage = new(T, bird.flock)
cage.cage_mob(target)
..()
@@ -0,0 +1,148 @@
/datum/action/cooldown/flock/control_drone
name = "Control Drone"
desc = "Direct a drone to perform an action."
button_icon_state = "ping"
click_to_activate = TRUE
unset_after_click = FALSE
click_cd_override = 0
var/mob/living/basic/flock/drone/selected_bird
/datum/action/cooldown/flock/control_drone/Remove(mob/removed_from)
. = ..()
free_drone()
/datum/action/cooldown/flock/control_drone/is_valid_target(atom/cast_on)
. = ..()
if(selected_bird)
return get_turf(cast_on)
if(!isflockdrone(cast_on))
return FALSE
var/mob/living/basic/flock/drone/bird = cast_on
if(HAS_TRAIT_FROM(bird, TRAIT_AI_DISABLE_PLANNING, FLOCK_CONTROLLED_BY_OVERMIND_SOURCE))
return FALSE
if(bird.controlled_by)
return FALSE
return TRUE
/datum/action/cooldown/flock/control_drone/Activate(atom/target)
. = ..()
if(isnull(selected_bird))
bind_drone(target)
return TRUE
selected_bird.ai_controller.cancel_actions()
// Move to turf/structure, or convert turf.
if(isturf(target) || istype(target, /obj/structure/flock))
if(istype(target, /obj/structure/flock/tealprint))
if(!selected_bird.ai_controller.queue_behavior(/datum/ai_behavior/flock/find_deposit_target, target))
return FALSE
pointer_helper(selected_bird, target, 2 SECONDS)
unset_click_ability(owner, performing_task = TRUE)
return TRUE
var/turf/T = get_turf(target)
if(isflockturf(T))
if(!selected_bird.ai_controller.queue_behavior(/datum/ai_behavior/flock/rally, target))
return FALSE
pointer_helper(selected_bird, target, 2 SECONDS)
unset_click_ability(owner, performing_task = TRUE)
return TRUE
if(T.can_flock_convert())
if(!selected_bird.ai_controller.queue_behavior(/datum/ai_behavior/flock/find_conversion_target, target))
return FALSE
pointer_helper(selected_bird, target, 2 SECONDS)
unset_click_ability(owner, performing_task = TRUE)
return TRUE
// Harvest items
else if(isitem(target))
if(!selected_bird.ai_controller.queue_behavior(/datum/ai_behavior/flock/find_harvest_target, target))
return FALSE
pointer_helper(selected_bird, target, 2 SECONDS)
unset_click_ability(owner, performing_task = TRUE)
return TRUE
// Attack or convert enemies.
else if(ismob(target) && selected_bird.flock.is_mob_enemy(target))
var/mob/living/L = target
if(L.incapacitated(ignore_restraints = TRUE, ignore_grab = TRUE) || L.IsKnockedDown())
if(!selected_bird.ai_controller.queue_behavior(/datum/ai_behavior/flock/find_capture_target, target))
return FALSE
pointer_helper(selected_bird, target, 2 SECONDS)
unset_click_ability(owner, performing_task = TRUE)
return TRUE
else if(selected_bird.ai_controller.queue_behavior(/datum/ai_behavior/flock/attack_target, target))
pointer_helper(selected_bird, target, 2 SECONDS)
unset_click_ability(owner, performing_task = TRUE)
return TRUE
return FALSE
else if(isflockmob(target))
if(istype(target, /mob/living/basic/flock/bit))
to_chat(owner, SPAN_WARNING("Flockbits are too simple to be remotely controlled."))
return FALSE
var/mob/living/basic/flock/other_bird = target
if(other_bird.flock == selected_bird.flock)
if(!selected_bird.ai_controller.queue_behavior(/datum/ai_behavior/flock/find_heal_target, target))
return FALSE
pointer_helper(selected_bird, target, 2 SECONDS)
unset_click_ability(owner, performing_task = TRUE)
return TRUE
/datum/action/cooldown/flock/control_drone/unset_click_ability(mob/on_who, refund_cooldown, performing_task = TRUE)
. = ..()
free_drone(performing_task)
/datum/action/cooldown/flock/control_drone/proc/pointer_helper(atom/from, atom/towards, duration)
var/mob/camera/flock/overmind/ghost_bird = owner
var/image/pointer = pointer_image_to(from, towards)
animate(pointer, time = 2 SECONDS, alpha = 0)
ghost_bird.flock.add_ping_image(ghost_bird.client, pointer, 2 SECONDS)
/datum/action/cooldown/flock/control_drone/proc/bind_drone(mob/living/basic/flock/drone/bird)
selected_bird = bird
RegisterSignal(bird, COMSIG_PARENT_QDELETING, PROC_REF(drone_gone))
ADD_TRAIT(bird, TRAIT_AI_DISABLE_PLANNING, FLOCK_CONTROLLED_BY_OVERMIND_SOURCE)
bird.ai_controller.cancel_actions()
bird.say("suspending automated subroutines pending sentient-level instruction", forced = TRUE)
bird.AddComponent(/datum/component/flock_ping/selected)
/datum/action/cooldown/flock/control_drone/proc/free_drone(performing_task = TRUE)
if(!selected_bird)
return
if(!QDELETED(selected_bird) && !performing_task)
spawn(-1)
selected_bird.say("Sentient-level instruction suspended, resuming automated subroutines.", forced = TRUE)
UnregisterSignal(selected_bird, COMSIG_PARENT_QDELETING)
REMOVE_TRAIT(selected_bird, TRAIT_AI_DISABLE_PLANNING, FLOCK_CONTROLLED_BY_OVERMIND_SOURCE)
qdel(selected_bird.GetComponent(/datum/component/flock_ping/selected))
selected_bird = null
unset_click_ability(owner)
/datum/action/cooldown/flock/control_drone/proc/drone_gone(datum/source)
SIGNAL_HANDLER
free_drone()
@@ -0,0 +1,27 @@
/datum/action/cooldown/flock/create_rift
name = "Spawn Rift"
desc = "Spawn an rift where you are, and from there, begin."
button_icon_state = "spawn_egg"
/datum/action/cooldown/flock/create_rift/is_valid_target(atom/cast_on)
var/turf/T = get_turf(owner)
if(!T.is_safe())
to_chat(owner, SPAN_WARNING("This place is not safe enough for a rift."))
return FALSE
if(T.is_blocked_turf())
to_chat(owner, SPAN_WARNING("There is not enough room for a rift here."))
return FALSE
return TRUE
/datum/action/cooldown/flock/create_rift/Activate(atom/target)
. = ..()
var/turf/T = get_turf(owner)
if(tgui_alert(owner, "Would you like to create the rift on [T]?", "Spawn Rift", list("Yes", "No")) != "Yes")
return
var/mob/camera/flock/overmind/ghost_bird = owner
ghost_bird.spawn_rift(T)
@@ -0,0 +1,49 @@
/datum/action/cooldown/flock/create_structure
name = "Place Tealprint"
desc = "Create a structure tealprint for your drones to construct onto."
button_icon_state = "fabstructure"
/datum/action/cooldown/flock/create_structure/is_valid_target(atom/cast_on)
var/turf/simulated/floor/flock/T = get_turf(owner)
if(!istype(T))
to_chat(owner, SPAN_WARNING("We are unable to perform complex substrate manipulation outside of our tiles."))
return FALSE
if(T.broken)
to_chat(owner, SPAN_WARNING("The tile we are above is broken."))
return FALSE
if(locate(/obj/structure/flock/tealprint) in T)
to_chat(owner, SPAN_WARNING("We are currently building something there."))
return FALSE
if(locate(/obj/structure/flock) in T)
to_chat(owner, SPAN_WARNING("An existing structure is occupying that tile."))
return FALSE
return TRUE
/datum/action/cooldown/flock/create_structure/Activate(atom/target)
var/mob/camera/flock/overmind/ghost_bird = owner
var/datum/flock/F = ghost_bird.flock
var/turf/loc = get_turf(ghost_bird)
var/options = list()
for(var/datum/flock_unlockable/unlockable as anything in F.unlockables)
if(!unlockable.unlocked)
continue
options[unlockable.name] = unlockable
var/datum/flock_unlockable/desired = tgui_input_list(ghost_bird, "Select a structure to create. Bandwidth upkeep costs are provided in parenthesis.", "Tealprint Selection", options)
desired = options[desired]
if(isnull(desired))
return
F.refresh_unlockables()
if(!desired.unlocked)
return
F.create_structure(loc, desired.structure_type)
return ..()
@@ -0,0 +1,43 @@
/datum/action/cooldown/flock/deconstruct
name = "Deconstruct"
click_to_activate = TRUE
check_flags = AB_CHECK_CONSCIOUS
/datum/action/cooldown/flock/deconstruct/is_valid_target(atom/cast_on)
var/turf/clicked_atom_turf = get_turf(cast_on)
if(!clicked_atom_turf)
return FALSE
if(get_dist(owner, clicked_atom_turf) > 1)
return FALSE
if(ismob(cast_on) || !HAS_TRAIT(cast_on, TRAIT_FLOCK_THING) || HAS_TRAIT(cast_on, TRAIT_FLOCK_NODECON))
return FALSE
return TRUE
/datum/action/cooldown/flock/deconstruct/Activate(atom/target)
. = ..()
var/mob/living/basic/flock/bird = owner
astype(bird, /mob/living/basic/flock/drone)?.stop_flockphase(TRUE)
bird.face_atom(target)
bird.visible_message(SPAN_NOTICE("[bird] begins to deconstruct [target]."), blind_message = SPAN_HEAR("You hear an otherwordly whirring."))
if(!do_after(bird, 6 SECONDS, target = target, interaction_key = "flock_deconstruct"))
return FALSE
if(!is_valid_target(target))
return FALSE
// Structures
if(isobj(target))
var/obj/obj_target = target
obj_target.deconstruct(TRUE)
return TRUE
else if(isflockturf(target))
var/turf/turf = target
turf.ChangeTurf(/turf/simulated/floor/flock, ignore_air = TRUE)
else
CRASH("Tried to flock deconstruct incompatible object of type: [target.type]")
return TRUE
@@ -0,0 +1,23 @@
/datum/action/cooldown/flock/deposit
name = "Deposit"
click_to_activate = TRUE
check_flags = AB_CHECK_CONSCIOUS
/datum/action/cooldown/flock/deposit/is_valid_target(atom/cast_on)
return istype(cast_on, /obj/structure/flock/tealprint)
/datum/action/cooldown/flock/deposit/Activate(atom/target)
. = ..()
var/mob/living/basic/flock/bird = owner
astype(bird, /mob/living/basic/flock/drone)?.stop_flockphase(TRUE)
bird.face_atom(target)
playsound(target, 'sound/goonstation/flockmind/flockdrone_quickbuild.ogg', 50, TRUE)
while(do_after(bird, 1 SECONDS, target = target, interaction_key = "flock_deposit"))
var/obj/structure/flock/tealprint/tealprint = target
var/deposit = min(bird.substrate.has_points(), tealprint.substrate.how_empty(), FLOCK_SUBSTRATE_COST_DEPOST_TEALPRINT)
bird.substrate.remove_points(deposit)
tealprint.substrate.add_points(deposit)
return TRUE
@@ -0,0 +1,18 @@
/datum/action/cooldown/flock/designate_deconstruct
name = "Mark for Deconstruction"
desc = "Mark an existing flock structure for deconstruction, refunding some resources."
button_icon_state = "destroystructure"
click_to_activate = TRUE
unset_after_click = FALSE
click_cd_override = 0
/datum/action/cooldown/flock/designate_deconstruct/Activate(atom/target)
. = ..()
if(istype(target, /obj/structure/flock/tealprint))
var/obj/structure/flock/tealprint/tealprint = target
return tealprint.try_cancel_structure()
var/mob/camera/flock/ghost_bird = owner
return ghost_bird.flock.toggle_deconstruction_mark(target)
@@ -0,0 +1,65 @@
/datum/action/cooldown/flock/designate_enemy
name = "Designate Enemy"
desc = "Mark or unmark someone as an enemy."
button_icon_state = "designate_enemy"
click_to_activate = TRUE
unset_after_click = FALSE
click_cd_override = 0
/datum/action/cooldown/flock/designate_enemy/is_valid_target(atom/cast_on)
var/atom/movable/target_movable = cast_on
if(!istype(target_movable))
return FALSE
return !isflockmob(cast_on) && isliving(cast_on) || target_movable.has_buckled_mobs()
/datum/action/cooldown/flock/designate_enemy/Activate(atom/target)
var/mob/camera/flock/ghost_bird = owner
var/datum/flock/flock = ghost_bird.flock
if(!flock)
return FALSE
. = ..()
if(flock.is_mob_ignored(target))
flock.remove_ignore(target)
else if(flock.is_mob_enemy(target))
flock.remove_enemy(target)
return TRUE
flock.update_enemy(target)
return TRUE
/datum/action/cooldown/flock/designate_ignore
name = "Designate Ignore"
desc = "Designate someone to be ignored by your Flock."
button_icon_state = "designate_ignore"
click_to_activate = TRUE
unset_after_click = FALSE
/datum/action/cooldown/flock/designate_ignore/is_valid_target(atom/cast_on)
var/atom/movable/target_movable = cast_on
if(!istype(target_movable))
return FALSE
return !isflockmob(cast_on) && isliving(cast_on) || target_movable.has_buckled_mobs()
/datum/action/cooldown/flock/designate_ignore/Activate(atom/target)
var/mob/camera/flock/ghost_bird = owner
var/datum/flock/flock = ghost_bird.flock
if(!flock)
return FALSE
. = ..()
if(flock.is_mob_ignored(target))
flock.remove_ignore(target)
else if(flock.is_mob_enemy(target))
flock.remove_enemy(target)
return TRUE
flock.add_ignore(target)
return TRUE
@@ -0,0 +1,37 @@
/datum/action/cooldown/flock/designate_tile
name = "Designate Priority Tile"
desc = "Add or remove a tile to the urgent tiles the Flock should claim."
button_icon_state = "designate_tile"
click_to_activate = TRUE
unset_after_click = FALSE
click_cd_override = 0
/datum/action/cooldown/flock/designate_tile/is_valid_target(atom/cast_on)
var/turf/T = get_turf(cast_on)
return T?.can_flock_convert()
/datum/action/cooldown/flock/designate_tile/Activate(atom/target)
var/turf/turf_target = get_turf(target)
if(isflockturf(target))
to_chat(owner, SPAN_ALERT("That tile has already been converted by the flock."))
return FALSE
if(!turf_target.can_flock_convert())
to_chat(owner, SPAN_ALERT("The flock is unable to convert that."))
return FALSE
var/mob/camera/flock/ghost_bird = owner
if(!ghost_bird.flock.marked_for_conversion[turf_target])
if(ghost_bird.flock.turf_reservations[turf_target])
to_chat(ghost_bird, SPAN_ALERT("That tile is already scheduled for conversion."))
return FALSE
ghost_bird.flock.marked_for_conversion[turf_target] = TRUE
ghost_bird.flock.add_notice(turf_target, FLOCK_NOTICE_PRIORITY)
return TRUE
else
ghost_bird.flock.marked_for_conversion -= turf_target
ghost_bird.flock.remove_notice(turf_target, FLOCK_NOTICE_PRIORITY)
return TRUE
@@ -0,0 +1,23 @@
/datum/action/cooldown/flock/diffract_drone
name = "Diffract Drone"
desc = "Split a drone into flockbits, mindless automata that only convert whatever they find."
button_icon_state = "diffract"
click_to_activate = TRUE
/datum/action/cooldown/flock/diffract_drone/is_valid_target(atom/cast_on)
if(!isflockdrone(cast_on))
return FALSE
var/mob/living/basic/flock/drone/bird = cast_on
if(bird.stat == DEAD)
return FALSE
return TRUE
/datum/action/cooldown/flock/diffract_drone/PreActivate(atom/target)
. = ..()
/datum/action/cooldown/flock/diffract_drone/Activate(atom/target)
. = ..()
var/mob/living/basic/flock/drone/bird = target
bird.split_into_bits()
@@ -0,0 +1,3 @@
/datum/action/cooldown/flock
button_icon = 'icons/goonstation/hud/flock_ui.dmi'
background_icon = null
@@ -0,0 +1,9 @@
/datum/action/cooldown/flock/control_panel
name = "Flock Control Panel"
desc = "Open the Flock control panel."
button_icon_state = "radio_stun"
/datum/action/cooldown/flock/control_panel/Activate(atom/target)
. = ..()
var/mob/camera/flock/ghost_bird = owner
ghost_bird.flock.ui_interact(ghost_bird)
@@ -0,0 +1,67 @@
/datum/action/cooldown/flock/convert
name = "Convert"
click_to_activate = TRUE
check_flags = AB_CHECK_CONSCIOUS
/datum/action/cooldown/flock/convert/New(Target)
. = ..()
/datum/action/cooldown/flock/convert/Destroy()
return ..()
/datum/action/cooldown/flock/convert/is_valid_target(atom/cast_on)
var/turf/clicked_atom_turf = get_turf(cast_on)
if(!clicked_atom_turf)
return FALSE
// This breaks pathfinding :(
// if(!owner.CanReach(clicked_atom_turf))
// return FALSE
// So instead:
if(get_dist(owner, clicked_atom_turf) > 1)
return FALSE
if(!clicked_atom_turf.can_flock_convert())
return FALSE
return TRUE
/datum/action/cooldown/flock/convert/Activate(atom/target)
. = ..()
playsound(owner, 'sound/goonstation/flockmind/flockdrone_convert.ogg', 30, TRUE, extrarange = SILENCED_SOUND_EXTRARANGE)
var/turf/clicked_atom_turf = get_turf(target)
if(isfloorturf(clicked_atom_turf) || iswallturf(clicked_atom_turf))
return convert_turf(clicked_atom_turf)
return FALSE
/datum/action/cooldown/flock/convert/proc/convert_turf(turf/T)
if(!is_valid_target(T))
return FALSE
var/mob/living/basic/flock/bird = owner
var/obj/effect/temp_visual/flock_effect
if(iswallturf(T))
flock_effect = new /obj/effect/temp_visual/flock/wall_convert(T)
else
flock_effect = new /obj/effect/temp_visual/flock/floor_convert(T)
owner.face_atom(T)
bird.flock?.reserve_turf(bird, T)
if(!do_after(owner, 4.5 SECONDS, target = T, interaction_key = "flock_convert"))
qdel(flock_effect)
bird.flock?.free_turf(bird)
return FALSE
bird.flock?.free_turf(bird)
qdel(flock_effect)
bird.flock?.claim_turf(T)
return TRUE
@@ -0,0 +1,24 @@
/datum/action/cooldown/flock/flock_heal
name = "Repair"
click_to_activate = TRUE
/datum/action/cooldown/flock/flock_heal/Activate(atom/target)
var/mob/living/basic/flock/drone/this_bird = owner
if(!this_bird.substrate.has_points(FLOCK_SUBSTRATE_COST_REPAIR))
return FALSE
astype(this_bird, /mob/living/basic/flock/drone)?.stop_flockphase(TRUE)
if(isflockmob(target))
var/mob/living/basic/flock/bird = target
ADD_TRAIT(bird, TRAIT_AI_PAUSED, ref(src))
if(!do_after(owner, 1 SECONDS, target = target, interaction_key = "flock_repair"))
REMOVE_TRAIT(bird, TRAIT_AI_PAUSED, ref(src))
return FALSE
REMOVE_TRAIT(bird, TRAIT_AI_PAUSED, ref(src))
bird.heal_overall_damage(10, 10)
this_bird.substrate.remove_points(FLOCK_SUBSTRATE_COST_REPAIR)
..()
return TRUE
@@ -0,0 +1,30 @@
/datum/action/cooldown/flock/repair_burst
name = "Concentrated Repair Burst"
desc = "Transmit large amounts of energy into up to four drones in a small area, healing them up to half of their hitpoints. Does not work on dead drones."
button_icon_state = "heal_drone"
cooldown_time = 30 SECONDS
click_to_activate = TRUE
/datum/action/cooldown/flock/repair_burst/Activate(atom/target)
var/list/targets = list()
for(var/mob/living/basic/flock/drone/bird in range(3, get_turf(target)))
if(bird.stat == DEAD)
continue
targets += bird
if(length(targets) >= 4)
break
if(!length(targets))
to_chat(owner, SPAN_ALERT("No repairable drones in range."))
return FALSE
. = ..()
to_chat(owner, SPAN_NOTICE("You transmit energy into your drones!"))
playsound(owner, 'sound/goonstation/flockmind/flockmind_cast.ogg', 50)
for(var/mob/living/basic/flock/drone/bird as anything in targets)
playsound(bird, "sound/effects/radio_sweep[rand(1,5)].ogg", 50, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
new /obj/effect/temp_visual/flock/flock_heal_burst(get_turf(bird))
bird.adjustHealth(-50)
@@ -0,0 +1,44 @@
/datum/action/cooldown/flock/nest
name = "Lay Egg"
button_icon_state = "spawn_egg"
check_flags = AB_CHECK_CONSCIOUS
/datum/action/cooldown/flock/nest/is_valid_target(atom/cast_on)
var/mob/living/basic/flock/drone/bird = owner
var/turf/simulated/floor/flock/flockfloor = get_turf(cast_on)
if(!istype(flockfloor) || flockfloor.is_blocked_turf(source_atom = owner))
to_chat(owner, SPAN_WARNING("The egg must be placed on an open flock tile."))
return FALSE
if(!bird.flock)
to_chat(owner, SPAN_WARNING("Our prime directives prohibit us from synthesizing an egg."))
return FALSE
if(!bird.substrate.has_points(bird.flock.current_egg_cost))
to_chat(owner, SPAN_WARNING("Error: Not enough resources. (We need [bird.flock.current_egg_cost])"))
return FALSE
if(HAS_TRAIT(bird, TRAIT_FLOCKPHASE))
to_chat(owner, SPAN_WARNING("We can not synthesize eggs while flockrunning."))
return FALSE
return TRUE
/datum/action/cooldown/flock/nest/Activate(atom/target)
. = ..()
var/mob/living/basic/flock/drone/bird = owner
if(!bird.substrate.has_points(bird.flock?.current_egg_cost || FLOCK_SUBSTRATE_COST_LAY_EGG))
return FALSE
var/turf/simulated/floor/flock/flockfloor = get_turf(target)
bird.stop_flockphase(TRUE)
to_chat(bird, SPAN_NOTICE("Our internal fabricators spring into action, we must hold still."))
if(!do_after_once(bird, 8 SECONDS, needhand = FALSE, target = bird.loc, interaction_key = "flock_lay_egg"))
return FALSE
bird.substrate.remove_points(bird.flock.current_egg_cost)
bird.visible_message(SPAN_NOTICE("[bird] deploys some sort of device."))
new /obj/structure/flock/egg(flockfloor)
return TRUE
@@ -0,0 +1,14 @@
/datum/action/cooldown/flock/ping
name = "Ping"
desc = "Request attention from other partitions of the Flock."
button_icon_state = "ping"
cooldown_time = 0.3 SECONDS
click_to_activate = TRUE
/datum/action/cooldown/flock/ping/is_valid_target(atom/cast_on)
return get_turf(cast_on)
/datum/action/cooldown/flock/ping/Activate(atom/target)
. = ..()
var/mob/camera/flock/ghost_bird = owner
ghost_bird.flock.ping(target, ghost_bird)
@@ -0,0 +1,44 @@
/datum/action/cooldown/flock/gatecrash
name = "Gatecrash"
desc = "Transmit an override signal to open every door within range."
button_icon_state = "open_door"
cooldown_time = 40 SECONDS
/datum/action/cooldown/flock/gatecrash/Activate(atom/target)
var/list/targets = list()
for(var/obj/machinery/door/airlock/airlock in range(10, get_turf(owner)))
if(airlock.canAIControl())
targets += airlock
for(var/obj/mecha/mech in range(10, get_turf(owner)))
if(mech.occupant)
targets += mech
if(!length(targets))
to_chat(owner, SPAN_NOTICE("No targets in range that can be opened via radio."))
return FALSE
..()
to_chat(owner, SPAN_NOTICE("You force open all powered doors around you."))
playsound(owner, 'sound/goonstation/flockmind/flockmind_cast.ogg', 50)
addtimer(CALLBACK(src, PROC_REF(async_open_doors), targets), 1.5 SECONDS)
return TRUE
/datum/action/cooldown/flock/gatecrash/proc/async_open_doors(list/doors)
for(var/obj/target in doors)
if(QDELETED(target))
continue
// Stagger openins a little randomly
if(prob(20))
sleep(0.2 SECONDS)
if(isairlock(target))
var/obj/machinery/door/airlock/airlock = target
INVOKE_ASYNC(airlock, TYPE_PROC_REF(/obj/machinery/door, open))
if(ismecha(target))
var/obj/mecha/mech = target
INVOKE_ASYNC(mech, TYPE_PROC_REF(/obj/mecha, go_out))
@@ -0,0 +1,83 @@
/datum/action/cooldown/flock/partition_mind
name = "Partition Mind"
button_icon_state = "partition_mind"
cooldown_time = 60 SECONDS
var/awaiting_partition = FALSE
/datum/action/cooldown/flock/partition_mind/New()
..()
desc = "Divide our computational power, creating a Flocktrace. Requires [FLOCK_COMPUTE_COST_FLOCKTRACE] total bandwidth per trace."
/datum/action/cooldown/flock/partition_mind/is_valid_target(atom/cast_on)
var/mob/camera/flock/overmind/ghost_bird = owner
if(awaiting_partition)
to_chat(ghost_bird, SPAN_WARNING("We are currently partitioning."))
return FALSE
if(!ghost_bird.flock.can_afford(FLOCK_COMPUTE_COST_FLOCKTRACE))
to_chat(ghost_bird, SPAN_WARNING("The Flock does not have enough spare computaional power to support another thread."))
return FALSE
if(length(ghost_bird.flock.traces) >= ghost_bird.flock.max_traces)
if(length(ghost_bird.flock.traces) < floor(FLOCK_COMPUTE_COST_RELAY / FLOCK_COMPUTE_COST_FLOCKTRACE))
to_chat(ghost_bird, SPAN_WARNING("The Flock needs more total computational power to support another thread."))
else
to_chat(ghost_bird, SPAN_WARNING("The Flock has hit it's thread limit."))
return FALSE
return TRUE
/datum/action/cooldown/flock/partition_mind/Activate(atom/target)
. = ..()
do_partition()
/datum/action/cooldown/flock/partition_mind/proc/do_partition()
set waitfor = FALSE
var/mob/camera/flock/overmind/ghost_bird = owner
if(!ghost_bird.flock.can_afford(FLOCK_COMPUTE_COST_FLOCKTRACE))
to_chat(ghost_bird, SPAN_WARNING("Partition failure: bandwidth required is unavailable."))
return
awaiting_partition = TRUE
to_chat(ghost_bird, SPAN_NOTICE("Partitioning initiated, stand by..."))
log_admin("Sending Flocktrace offer to ghosts, they have [30 SECONDS] to respond.")
var/list/candidates = SSghost_spawns.poll_candidates("Do you want to play as a Flocktrace?", ROLE_FLOCK, TRUE, source = /mob/camera/flock/trace)
awaiting_partition = FALSE
if(QDELETED(owner))
log_admin("The Flockmind lost before the partition could complete.")
return
if(!length(candidates))
log_admin("No ghosts responded to the flocktrace offer from [ghost_bird.real_name]")
to_chat(ghost_bird, SPAN_WARNING("Partition failure: unable to coalesce sentience."))
return
if(!ghost_bird.flock.can_afford(FLOCK_COMPUTE_COST_FLOCKTRACE))
log_admin("The Flock was unable to support another flocktrace, partition aborted.")
to_chat(ghost_bird, SPAN_WARNING("Partition failure: bandwidth required is unavailable."))
return
var/mob/candidate = pick(candidates)
dust_if_respawnable(candidate)
log_admin("[key_name_admin(candidate)] respawned as a Flocktrace.")
var/mob/camera/flock/trace/new_ghostbird = new(get_turf(ghost_bird), ghost_bird.flock)
new_ghostbird.key = candidate.key
new_ghostbird.mind = new
new_ghostbird.mind.bind_to(new_ghostbird)
new_ghostbird.mind.set_original_mob(new_ghostbird)
new_ghostbird.mind.assigned_role = SPECIAL_ROLE_FLOCK
new_ghostbird.mind.special_role = SPECIAL_ROLE_FLOCK
var/list/messages = list()
messages += "<div style='font-size: 200%;text-align: center'>You are [gradient_text("The Divine Flock","#3cb5a3", "#124e43")]</div>"
messages += "<div style='text-align: center'>" + gradient_text("We have been partitioned by our overmind to further their goals of propogating The Signal. It is our task to assist in managing their drones to achieve their goals. Such is the will of the Monarch.", "#3cb5a3", "#1e806e") + "</div>"
messages += "<div style='text-align: center'>" + gradient_text("Our powers are more limited than that of our overmind, but we can utilize gatecrash subsystems and take manual control of drones (using alt-click).", "#3cb5a3", "#1e806e") + "</div>"
messages += "<div style='text-align: center'>" + gradient_text("For more information, we have decrypted an information manifest: ([GLOB.configuration.url.wiki_url]/index.php/The_Divine_Flock)", "#3cb5a3", "#1e806e") + "</div>"
to_chat(new_ghostbird, chat_box_red(messages.Join("<br>")))
new_ghostbird.playsound_local(new_ghostbird, 'sound/goonstation/flockmind/ArtifactFea2.ogg', 50, FALSE, use_reverb = FALSE)
SSticker.mode.traitors |= new_ghostbird.mind
@@ -0,0 +1,39 @@
/datum/action/cooldown/flock/radio_blast
name = "Radio Stun Burst"
desc = "Overwhelm the radio headsets of everyone within 3m of your target. Will not work on broken or non-existent headsets."
button_icon_state = "radio_stun"
cooldown_time = 20 SECONDS
click_to_activate = TRUE
/datum/action/cooldown/flock/radio_blast/Activate(atom/target)
var/list/targets = list()
for(var/mob/living/carbon/human/guy in range(3, get_turf(target)))
var/obj/item/radio/worn_radio
for(var/obj/item/I in guy.get_contents())
if(istype(I, /obj/item/radio))
worn_radio = I
break
if(!istype(worn_radio) || !guy.can_hear() || !worn_radio.listening)
continue
targets += guy
if(!length(targets))
to_chat(owner, SPAN_ALERT("No targets are in range with active headsets."))
return FALSE
. = ..()
to_chat(owner, SPAN_NOTICE("You transmit the worst static you can weave into the headsets around you."))
playsound(owner, 'sound/goonstation/flockmind/flockmind_cast.ogg', 50, FALSE)
for(var/mob/living/carbon/human/guy as anything in targets)
to_chat(guy, SPAN_ALERT("Horrifying static bursts into your headset, disorienting you severely!"))
playsound(guy, "sound/effects/radio_sweep[rand(1,5)].ogg", 50, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
guy.KnockDown(3 SECONDS)
guy.SetConfused(20 SECONDS)
guy.do_jitter_animation(10)
var/obj/item/organ/internal/ears/ears = guy.get_int_organ(/obj/item/organ/internal/ears)
if(istype(ears))
ears.receive_damage(rand(3, 6))
@@ -0,0 +1,40 @@
/datum/action/cooldown/flock/radio_talk
name = "Narrowbeam Transmission"
desc = "Hijack a radio headset of a target to broadcast a message."
button_icon_state = "talk"
cooldown_time = 2 SECONDS
click_to_activate = TRUE
/datum/action/cooldown/flock/radio_talk/Activate(atom/target)
var/obj/item/radio/worn_radio
if(!ishuman(target) && !istype(target, /obj/item/radio))
to_chat(owner, SPAN_ALERT("[target] is not a valid target!"))
return FALSE
if(istype(target, /obj/item/radio))
worn_radio = target
if(!worn_radio.listening)
to_chat(owner, SPAN_ALERT("[target] is not turned on!"))
return FALSE
if(ishuman(target))
var/mob/living/carbon/human/schmuck = target
for(var/obj/item/I in schmuck.get_contents())
if(istype(I, /obj/item/radio))
worn_radio = I
break
if(!istype(worn_radio) || !worn_radio.listening)
to_chat(owner, SPAN_ALERT("[target] does not have a working radio!"))
return FALSE
var/msg = tgui_input_text(usr, "What do you wish to broadcast?", null, "")
if(!msg)
return FALSE
. = ..()
playsound(get_turf(worn_radio), "sound/effects/radio_sweep[rand(1,5)].ogg", 20, TRUE, SHORT_RANGE_SOUND_EXTRARANGE)
to_chat(owner, SPAN_NOTICE("You transmit a message through [worn_radio]!"))
worn_radio.autosay(istype(target, /obj/item/radio) ? Gibberish(msg, 70, 50) : msg, "Strange Static", null)
@@ -0,0 +1,13 @@
/datum/action/cooldown/flock/release_control
name = "Release Control"
desc = "Release yourself from this drone."
button_icon_state = "eject"
/datum/action/cooldown/flock/release_control/Activate(atom/target)
. = ..()
var/mob/living/basic/flock/drone/bird = owner
if(!bird.flock)
to_chat(bird, SPAN_WARNING("You have no flock to return to."))
return
bird.release_control(TRUE)
@@ -0,0 +1,19 @@
/datum/ai_behavior/flock
/// The name of the behavior in the UI for flock drones.
var/name = ""
/datum/ai_behavior/flock/perform(seconds_per_tick, datum/ai_controller/controller, ...)
SHOULD_CALL_PARENT(TRUE)
. = ..()
var/mob/living/basic/flock/drone/bird = controller.pawn
bird.set_task_desc(name)
/datum/ai_behavior/flock/finish_action(datum/ai_controller/controller, succeeded, ...)
var/mob/living/basic/flock/drone/bird = controller.pawn
if(HAS_TRAIT(controller.pawn, TRAIT_FLOCKPHASE))
bird.stop_flockphase(FALSE)
. = ..()
bird.set_task_desc("")
@@ -0,0 +1,86 @@
/datum/ai_behavior/flock/find_capture_target
name = "capturing"
goap_weight = FLOCK_BEHAVIOR_WEIGHT_CAPTURE
/datum/ai_behavior/flock/find_capture_target/setup(datum/ai_controller/controller, mob/overmind_target)
. = ..()
if(overmind_target)
var/mob/living/basic/flock/drone/bird = controller.pawn
if(isturf(overmind_target.loc))
controller.set_blackboard_key(BB_FLOCK_OVERMIND_CONTROL, TRUE)
controller.set_blackboard_key(BB_PATH_MAX_LENGTH, 200)
bird.say("instruction confirmed: capture biological lifeform", forced = TRUE)
else
bird.say("invalid capture target provided by sentient-level instruction", forced = TRUE)
return FALSE
/datum/ai_behavior/flock/find_capture_target/goap_precondition(datum/ai_controller/controller)
var/mob/living/basic/flock/drone/bird = controller.pawn
return length(bird.flock?.enemies)
/datum/ai_behavior/flock/find_capture_target/goap_get_potential_targets(datum/ai_controller/controller)
var/mob/living/basic/flock/bird = controller.pawn
return bird.flock.enemies.Copy()
/datum/ai_behavior/flock/find_capture_target/goap_is_valid_target(datum/ai_controller/controller, atom/target)
var/mob/living/target_mob = target
return ismob(target_mob) && isturf(target_mob.loc) && (target_mob.incapacitated(ignore_restraints = TRUE, ignore_grab = TRUE) || target_mob.IsKnockedDown())
/datum/ai_behavior/flock/find_capture_target/perform(seconds_per_tick, datum/ai_controller/controller, mob/overmind_target)
..()
var/atom/target = overmind_target || goap_get_ideal_target(controller, set_path = TRUE)
if(!target)
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
controller.set_blackboard_key(BB_FLOCK_CAPTURE_TARGET, target)
return AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/flock/find_capture_target/finish_action(datum/ai_controller/controller, succeeded, obj/item/overmind_target)
. = ..()
controller.clear_blackboard_key(BB_PATH_TO_USE)
if(!succeeded && overmind_target)
controller.clear_blackboard_key(BB_PATH_MAX_LENGTH)
controller.clear_blackboard_key(BB_FLOCK_OVERMIND_CONTROL)
/datum/ai_behavior/flock/find_capture_target/next_behavior(datum/ai_controller/controller, success)
if(success)
controller.queue_behavior(/datum/ai_behavior/flock/perform_capture)
/datum/ai_behavior/flock/perform_capture
name = "capturing"
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH
/datum/ai_behavior/flock/perform_capture/setup(datum/ai_controller/controller, ...)
var/atom/target = controller.blackboard[BB_FLOCK_CAPTURE_TARGET]
set_movement_target(controller, target)
return ..()
/datum/ai_behavior/flock/perform_capture/perform(seconds_per_tick, datum/ai_controller/controller, ...)
..()
var/mob/living/basic/flock/drone/bird = controller.pawn
var/mob/living/target = controller.blackboard[BB_FLOCK_CAPTURE_TARGET]
if(target)
if(!isturf(target?.loc) || !(target.incapacitated(ignore_restraints = TRUE, ignore_grab = TRUE)) || target.IsKnockedDown())
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
controller.clear_blackboard_key(BB_FLOCK_CAPTURE_TARGET)
var/datum/action/cooldown/flock/cage_mob/cage_action = locate() in bird.actions
spawn(-1)
cage_action.Trigger(target = target)
if(DOING_INTERACTION(bird, "flock_cage"))
return AI_BEHAVIOR_DELAY
return AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/flock/perform_capture/finish_action(datum/ai_controller/controller, succeeded, ...)
. = ..()
controller.clear_blackboard_key(BB_FLOCK_CAPTURE_TARGET)
if(!succeeded && controller.blackboard[BB_FLOCK_OVERMIND_CONTROL] && !QDELETED(controller.pawn))
var/mob/living/basic/flock/bird = controller.pawn
bird.say("unable to reach target provided by sentient level instruction, aborting subroutine", forced = TRUE)
controller.clear_blackboard_key(BB_PATH_MAX_LENGTH)
controller.clear_blackboard_key(BB_FLOCK_OVERMIND_CONTROL)
@@ -0,0 +1,158 @@
/datum/ai_behavior/flock/find_conversion_target
name = "building"
goap_weight
/datum/ai_behavior/flock/find_conversion_target/setup(datum/ai_controller/controller, turf/overmind_target)
. = ..()
if(overmind_target)
var/mob/living/basic/flock/drone/bird = controller.pawn
if(goap_is_valid_target(controller, overmind_target))
controller.set_blackboard_key(BB_FLOCK_OVERMIND_CONTROL, TRUE)
controller.set_blackboard_key(BB_PATH_MAX_LENGTH, 200)
bird.say("instruction confirmed: convert object to substrate", forced = TRUE)
else
bird.say("invalid conversion target provided by sentient-level instruction", forced = TRUE)
return FALSE
/datum/ai_behavior/flock/find_conversion_target/goap_precondition(datum/ai_controller/controller)
var/mob/living/basic/flock/bird = controller.pawn
return bird.substrate.has_points(FLOCK_SUBSTRATE_COST_CONVERT)
/datum/ai_behavior/flock/find_conversion_target/score_distance(datum/ai_controller/controller, atom/target)
. = ..()
var/mob/living/basic/flock/bird = controller.pawn
if(bird.flock?.marked_for_conversion[target])
/*
* because the result of scoring is based on max distance,
* the score of any given tile is -100 to 0, with 0 being best.
* Adding 200 basically allows a tile at twice the max distance to be considered.
*/
. += 200
/datum/ai_behavior/flock/find_conversion_target/goap_get_potential_targets(datum/ai_controller/controller)
var/mob/living/basic/flock/bird = controller.pawn
var/datum/flock/bird_flock = bird.flock
var/list/options = list()
var/list/priority_turfs = bird_flock?.get_priority_turfs()
if(length(priority_turfs))
options += priority_turfs
for(var/turf/T as turf in range(controller.target_search_radius, bird))
options += T
return options
/datum/ai_behavior/flock/find_conversion_target/goap_is_valid_target(datum/ai_controller/controller, atom/target)
var/turf/T = target
if(!isturf(T))
return FALSE
if(!T.can_flock_convert())
return FALSE
if(isflockturf(T))
return FALSE
var/mob/living/basic/flock/bird = controller.pawn
if(isnull(bird.flock))
return TRUE
if(bird.flock.claimed_floors[T] || bird.flock.claimed_walls[T])
return FALSE
return bird.flock.is_turf_free(T)
/datum/ai_behavior/flock/find_conversion_target/perform(seconds_per_tick, datum/ai_controller/controller, turf/overmind_target)
..()
var/turf/target = overmind_target || goap_get_ideal_target(controller, TRUE)
if(!target)
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
controller.set_blackboard_key(BB_FLOCK_CONVERT_TARGET, target)
var/mob/living/basic/flock/bird = controller.pawn
if(bird.flock)
bird.flock.reserve_turf(bird, target)
return AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/flock/find_conversion_target/finish_action(datum/ai_controller/controller, succeeded, turf/overmind_target)
. = ..()
controller.clear_blackboard_key(BB_PATH_TO_USE)
if(!succeeded && overmind_target)
controller.clear_blackboard_key(BB_PATH_MAX_LENGTH)
controller.clear_blackboard_key(BB_FLOCK_OVERMIND_CONTROL)
/datum/ai_behavior/flock/find_conversion_target/next_behavior(datum/ai_controller/controller, success)
if(success)
controller.queue_behavior(/datum/ai_behavior/flock/perform_conversion)
/datum/ai_behavior/flock/perform_conversion
name = "building"
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT
/datum/ai_behavior/flock/perform_conversion/setup(datum/ai_controller/controller, ...)
var/turf/target_turf = controller.blackboard[BB_FLOCK_CONVERT_TARGET]
set_movement_target(controller, target_turf)
return ..()
/datum/ai_behavior/flock/perform_conversion/perform(seconds_per_tick, datum/ai_controller/controller, ...)
..()
var/mob/living/basic/flock/bird = controller.pawn
var/turf/target = controller.blackboard[BB_FLOCK_CONVERT_TARGET]
if(target)
controller.clear_blackboard_key(BB_FLOCK_CONVERT_TARGET)
var/datum/action/cooldown/flock/convert/convert_action = locate() in bird.actions
spawn(-1)
convert_action.Trigger(target = target)
if(DOING_INTERACTION(bird, "flock_convert"))
return AI_BEHAVIOR_DELAY
return AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/flock/perform_conversion/finish_action(datum/ai_controller/controller, succeeded, ...)
. = ..()
var/mob/living/basic/flock/drone/bird = controller.pawn
bird.flock?.free_turf(bird)
if(!succeeded && controller.blackboard[BB_FLOCK_OVERMIND_CONTROL] && !QDELETED(controller.pawn))
bird.say("unable to reach target provided by sentient level instruction, aborting subroutine", forced = TRUE)
controller.clear_blackboard_key(BB_FLOCK_CONVERT_TARGET)
controller.clear_blackboard_key(BB_PATH_MAX_LENGTH)
controller.clear_blackboard_key(BB_FLOCK_OVERMIND_CONTROL)
//
// Subtype for creating a nest. Effectively a higher priority convert that only happens when the bird can lay an egg.
//
/datum/ai_behavior/flock/find_conversion_target/nest
name = "nesting"
goap_weight = FLOCK_BEHAVIOR_WEIGHT_NEST
required_distance = 0
/datum/ai_behavior/flock/find_conversion_target/nest/goap_precondition(datum/ai_controller/controller)
var/mob/living/basic/flock/bird = controller.pawn
if(!bird.flock)
return FALSE
if(length(bird.flock.drones) > FLOCK_DRONE_LIMIT)
return FALSE
return bird.substrate.has_points(FLOCK_SUBSTRATE_COST_CONVERT + bird.flock.current_egg_cost)
/datum/ai_behavior/flock/find_conversion_target/nest/goap_is_valid_target(datum/ai_controller/controller, atom/target)
var/turf/T = target
return ..() && !T.is_blocked_turf(exclude_mobs = TRUE) && !locate(/obj/structure/flock/egg, T)
/datum/ai_behavior/flock/perform_conversion/nest
name = "nesting"
required_distance = 0
/datum/ai_behavior/flock/perform_conversion/nest/next_behavior(datum/ai_controller/controller, success)
. = ..()
if(success)
controller.queue_behavior(/datum/ai_behavior/flock/find_existing_nest)
@@ -0,0 +1,114 @@
/datum/ai_behavior/flock/find_deconstruct_target
name = "deconstructing"
goap_weight = FLOCK_BEHAVIOR_WEIGHT_DECONSTRUCT
/datum/ai_behavior/flock/find_deconstruct_target/setup(datum/ai_controller/controller, turf/overmind_target)
. = ..()
if(overmind_target)
var/mob/living/basic/flock/drone/bird = controller.pawn
if(is_valid_target(overmind_target))
controller.set_blackboard_key(BB_FLOCK_OVERMIND_CONTROL, TRUE)
controller.set_blackboard_key(BB_PATH_MAX_LENGTH, 200)
bird.say("instruction confirmed: deconstruct object", forced = TRUE)
else
bird.say("invalid deconstruct target provided by sentient-level instruction", forced = TRUE)
return FALSE
/datum/ai_behavior/flock/find_deconstruct_target/goap_precondition(datum/ai_controller/controller)
var/mob/living/basic/flock/bird = controller.pawn
return length(bird.flock?.marked_for_deconstruction)
/datum/ai_behavior/flock/find_deconstruct_target/goap_score(datum/ai_controller/controller)
return score_distance(controller, get_target(controller))
/datum/ai_behavior/flock/find_deconstruct_target/score_distance(datum/ai_controller/controller, atom/target)
. = ..()
var/mob/living/basic/flock/bird = controller.pawn
if(bird.flock?.marked_for_deconstruction[target])
/*
* because the result of scoring is based on max distance,
* the score of any given tile is -100 to 0, with 0 being best.
* Adding 200 basically allows a tile at twice the max distance to be considered.
*/
. += 200
/datum/ai_behavior/flock/find_deconstruct_target/proc/get_target(datum/ai_controller/controller, path_to = FALSE)
var/mob/living/basic/flock/bird = controller.pawn
var/list/options = list()
for(var/atom/A as anything in bird.flock.marked_for_deconstruction)
if(get_dist(A, bird) > controller.max_target_distance)
continue
if(bird.flock && !bird.flock.is_turf_free(get_turf(A)))
continue
options += A
return get_best_target_by_distance_score(controller, options, path_to)
/datum/ai_behavior/flock/find_deconstruct_target/proc/is_valid_target(atom/target)
return !ismob(target) && HAS_TRAIT(target, TRAIT_FLOCK_THING) && !HAS_TRAIT(target, TRAIT_FLOCK_NODECON)
/datum/ai_behavior/flock/find_deconstruct_target/perform(seconds_per_tick, datum/ai_controller/controller, turf/overmind_target)
..()
var/atom/target = overmind_target || get_target(controller, TRUE)
if(!target)
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
controller.set_blackboard_key(BB_FLOCK_DECON_TARGET, target)
var/mob/living/basic/flock/bird = controller.pawn
if(bird.flock)
bird.flock.reserve_turf(bird, get_turf(target), remove_on_change = FALSE)
return AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/flock/find_deconstruct_target/finish_action(datum/ai_controller/controller, succeeded, turf/overmind_target)
. = ..()
controller.clear_blackboard_key(BB_PATH_TO_USE)
if(!succeeded && overmind_target)
controller.clear_blackboard_key(BB_PATH_MAX_LENGTH)
controller.clear_blackboard_key(BB_FLOCK_OVERMIND_CONTROL)
/datum/ai_behavior/flock/find_deconstruct_target/next_behavior(datum/ai_controller/controller, success)
if(success)
controller.queue_behavior(/datum/ai_behavior/flock/perform_deconstruct)
/datum/ai_behavior/flock/perform_deconstruct
name = "deconstructing"
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT
/datum/ai_behavior/flock/perform_deconstruct/setup(datum/ai_controller/controller, ...)
var/turf/target_turf = controller.blackboard[BB_FLOCK_DECON_TARGET]
set_movement_target(controller, target_turf)
return ..()
/datum/ai_behavior/flock/perform_deconstruct/perform(seconds_per_tick, datum/ai_controller/controller, ...)
..()
var/mob/living/basic/flock/bird = controller.pawn
var/turf/target = controller.blackboard[BB_FLOCK_DECON_TARGET]
if(target)
controller.clear_blackboard_key(BB_FLOCK_DECON_TARGET)
var/datum/action/cooldown/flock/deconstruct/deconstruct_action = locate() in bird.actions
spawn(-1)
deconstruct_action.Trigger(target = target)
if(DOING_INTERACTION(bird, "flock_deconstruct"))
return AI_BEHAVIOR_DELAY
return AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/flock/perform_deconstruct/finish_action(datum/ai_controller/controller, succeeded, ...)
. = ..()
var/mob/living/basic/flock/drone/bird = controller.pawn
bird.flock?.free_turf(bird)
if(!succeeded && controller.blackboard[BB_FLOCK_OVERMIND_CONTROL] && !QDELETED(controller.pawn))
bird.say("unable to reach target provided by sentient level instruction, aborting subroutine", forced = TRUE)
controller.clear_blackboard_key(BB_FLOCK_DECON_TARGET)
controller.clear_blackboard_key(BB_PATH_MAX_LENGTH)
controller.clear_blackboard_key(BB_FLOCK_OVERMIND_CONTROL)
@@ -0,0 +1,101 @@
/datum/ai_behavior/flock/find_deposit_target
name = "depositing"
goap_weight = FLOCK_BEHAVIOR_WEIGHT_DEPOSIT
/datum/ai_behavior/flock/find_deposit_target/setup(datum/ai_controller/controller, obj/structure/flock/overmind_target)
. = ..()
if(overmind_target)
var/mob/living/basic/flock/drone/bird = controller.pawn
if(goap_is_valid_target(controller, overmind_target))
controller.set_blackboard_key(BB_FLOCK_OVERMIND_CONTROL, TRUE)
controller.set_blackboard_key(BB_PATH_MAX_LENGTH, 200)
bird.say("instruction confirmed: deposit substrate", forced = TRUE)
else
bird.say("invalid deposit target provided by sentient-level instruction", forced = TRUE)
return FALSE
/datum/ai_behavior/flock/find_deposit_target/goap_precondition(datum/ai_controller/controller)
var/mob/living/basic/flock/bird = controller.pawn
return bird.substrate.has_points(FLOCK_SUBSTRATE_COST_DEPOST_TEALPRINT)
/datum/ai_behavior/flock/find_deposit_target/goap_get_potential_targets(datum/ai_controller/controller)
var/list/options = ..()
for(var/obj/structure/flock/tealprint in range(controller.target_search_radius, controller.pawn))
options += tealprint
return options
/datum/ai_behavior/flock/find_deposit_target/goap_is_valid_target(datum/ai_controller/controller, atom/target)
var/obj/structure/flock/tealprint/tealprint = target
if(!istype(tealprint))
return FALSE
var/mob/living/basic/flock/bird = controller.pawn
return (tealprint.flock == bird.flock) && !tealprint.substrate.is_full()
/datum/ai_behavior/flock/find_deposit_target/perform(seconds_per_tick, datum/ai_controller/controller, overmind_target)
..()
var/atom/target = overmind_target || goap_get_ideal_target(controller, set_path = TRUE)
if(!target)
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
controller.set_blackboard_key(BB_FLOCK_DEPOSIT_TARGET, target)
return AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/flock/find_deposit_target/finish_action(datum/ai_controller/controller, succeeded, overmind_target)
. = ..()
controller.clear_blackboard_key(BB_PATH_TO_USE)
if(!succeeded && overmind_target)
controller.clear_blackboard_key(BB_PATH_MAX_LENGTH)
controller.clear_blackboard_key(BB_FLOCK_OVERMIND_CONTROL)
/datum/ai_behavior/flock/find_deposit_target/next_behavior(datum/ai_controller/controller, success)
if(success)
controller.queue_behavior(/datum/ai_behavior/flock/perform_deposit)
/datum/ai_behavior/flock/perform_deposit
name = "depositing"
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH
/datum/ai_behavior/flock/perform_deposit/setup(datum/ai_controller/controller, ...)
var/turf/target_turf = controller.blackboard[BB_FLOCK_DEPOSIT_TARGET]
set_movement_target(controller, target_turf)
return ..()
/datum/ai_behavior/flock/perform_deposit/goap_is_valid_target(datum/ai_controller/controller, atom/target)
var/obj/structure/flock/tealprint/tealprint = target
if(!istype(tealprint))
return FALSE
var/mob/living/basic/flock/bird = controller.pawn
return (tealprint.flock == bird.flock) && !tealprint.substrate.is_full()
/datum/ai_behavior/flock/perform_deposit/perform(seconds_per_tick, datum/ai_controller/controller, ...)
..()
var/mob/living/basic/flock/drone/bird = controller.pawn
var/obj/structure/flock/tealprint/target = controller.blackboard[BB_FLOCK_DEPOSIT_TARGET]
if(target)
if(!goap_is_valid_target(controller, target))
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
controller.clear_blackboard_key(BB_FLOCK_DEPOSIT_TARGET)
var/datum/action/cooldown/flock/deposit/deposit_action = locate() in bird.actions
spawn(-1)
deposit_action.Trigger(target = target)
if(DOING_INTERACTION(bird, "flock_cage"))
return AI_BEHAVIOR_DELAY
return AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/flock/perform_deposit/finish_action(datum/ai_controller/controller, succeeded, ...)
. = ..()
controller.clear_blackboard_key(BB_FLOCK_DEPOSIT_TARGET)
if(!succeeded && controller.blackboard[BB_FLOCK_OVERMIND_CONTROL] && !QDELETED(controller.pawn))
var/mob/living/basic/flock/bird = controller.pawn
bird.say("unable to reach target provided by sentient level instruction, aborting subroutine", forced = TRUE)
controller.clear_blackboard_key(BB_PATH_MAX_LENGTH)
controller.clear_blackboard_key(BB_FLOCK_OVERMIND_CONTROL)
@@ -0,0 +1,88 @@
/datum/ai_behavior/flock/find_harvest_target
name = "harvesting"
goap_weight = FLOCK_BEHAVIOR_WEIGHT_HARVEST
/datum/ai_behavior/flock/find_harvest_target/setup(datum/ai_controller/controller, obj/item/overmind_target)
. = ..()
if(overmind_target)
var/mob/living/basic/flock/drone/bird = controller.pawn
if(isitem(overmind_target) && isturf(overmind_target.loc))
controller.set_blackboard_key(BB_FLOCK_OVERMIND_CONTROL, TRUE)
controller.set_blackboard_key(BB_PATH_MAX_LENGTH, 200)
bird.say("instruction confirmed: convert object to substrate", forced = TRUE)
else
bird.say("invalid harvest target provided by sentient-level instruction", forced = TRUE)
return FALSE
/datum/ai_behavior/flock/find_harvest_target/goap_precondition(datum/ai_controller/controller)
var/mob/living/basic/flock/drone/bird = controller.pawn
var/datum/flockdrone_part/absorber/absorber = locate() in bird.parts
return !absorber.held_item
/datum/ai_behavior/flock/find_harvest_target/goap_score(datum/ai_controller/controller)
return score_distance(controller, get_target(controller))
/datum/ai_behavior/flock/find_harvest_target/proc/get_target(datum/ai_controller/controller, path_to = FALSE)
var/mob/living/basic/flock/bird = controller.pawn
var/list/options = list()
for(var/obj/item/I in view(controller.target_search_radius, bird))
if(isturf(I.loc) && !I.anchored)
options += I
return get_best_target_by_distance_score(controller, options, path_to)
/datum/ai_behavior/flock/find_harvest_target/perform(seconds_per_tick, datum/ai_controller/controller, obj/item/overmind_target)
..()
var/atom/target = overmind_target || get_target(controller, TRUE)
if(!target)
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
controller.set_blackboard_key(BB_FLOCK_HARVEST_TARGET, target)
return AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/flock/find_harvest_target/finish_action(datum/ai_controller/controller, succeeded, obj/item/overmind_target)
. = ..()
controller.clear_blackboard_key(BB_PATH_TO_USE)
if(!succeeded && overmind_target)
controller.clear_blackboard_key(BB_PATH_MAX_LENGTH)
controller.clear_blackboard_key(BB_FLOCK_OVERMIND_CONTROL)
/datum/ai_behavior/flock/find_harvest_target/next_behavior(datum/ai_controller/controller, success)
if(success)
controller.queue_behavior(/datum/ai_behavior/flock/perform_harvest)
/datum/ai_behavior/flock/perform_harvest
name = "harvesting"
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH
/datum/ai_behavior/flock/perform_harvest/setup(datum/ai_controller/controller, ...)
var/turf/target_turf = controller.blackboard[BB_FLOCK_HARVEST_TARGET]
set_movement_target(controller, target_turf)
return ..()
/datum/ai_behavior/flock/perform_harvest/perform(seconds_per_tick, datum/ai_controller/controller, ...)
..()
var/mob/living/basic/flock/drone/bird = controller.pawn
var/obj/item/target = controller.blackboard[BB_FLOCK_HARVEST_TARGET]
if(!isturf(target?.loc))
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
var/datum/flockdrone_part/absorber/absorber = locate() in bird.parts
if(!absorber.try_pickup_item(target))
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
return AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/flock/perform_harvest/finish_action(datum/ai_controller/controller, succeeded, ...)
. = ..()
controller.clear_blackboard_key(BB_FLOCK_HARVEST_TARGET)
if(!succeeded && controller.blackboard[BB_FLOCK_OVERMIND_CONTROL] && !QDELETED(controller.pawn))
var/mob/living/basic/flock/bird = controller.pawn
bird.say("unable to reach target provided by sentient level instruction, aborting subroutine", forced = TRUE)
controller.clear_blackboard_key(BB_PATH_MAX_LENGTH)
controller.clear_blackboard_key(BB_FLOCK_OVERMIND_CONTROL)
@@ -0,0 +1,90 @@
/datum/ai_behavior/flock/find_heal_target
name = "repairing"
goap_weight = FLOCK_BEHAVIOR_WEIGHT_REPAIR
/datum/ai_behavior/flock/find_heal_target/setup(datum/ai_controller/controller, mob/living/basic/flock/overmind_target)
. = ..()
if(overmind_target)
var/mob/living/basic/flock/drone/bird = controller.pawn
if(goap_is_valid_target(controller, overmind_target))
controller.set_blackboard_key(BB_FLOCK_OVERMIND_CONTROL, TRUE)
controller.set_blackboard_key(BB_PATH_MAX_LENGTH, 200)
bird.say("instruction confirmed: repair construct", forced = TRUE)
else
bird.say("invalid repair target provided by sentient-level instruction", forced = TRUE)
return FALSE
/datum/ai_behavior/flock/find_heal_target/goap_precondition(datum/ai_controller/controller)
var/mob/living/basic/flock/drone/bird = controller.pawn
return bird.flock && bird.substrate.has_points(FLOCK_SUBSTRATE_COST_REPAIR)
/datum/ai_behavior/flock/find_heal_target/goap_get_potential_targets(datum/ai_controller/controller)
var/list/options = ..()
for(var/mob/living/basic/flock/drone/bird in oview(controller.target_search_radius, controller.pawn))
options += bird
return options
/datum/ai_behavior/flock/find_heal_target/goap_is_valid_target(datum/ai_controller/controller, atom/target)
var/mob/living/basic/flock/drone/this_bird = controller.pawn
var/mob/living/basic/flock/drone/other_bird = target
if(this_bird.flock != other_bird.flock)
return FALSE
if(other_bird.stat == DEAD)
return FALSE
// Birds at or below 60% health
if((other_bird.getBruteLoss() + other_bird.getFireLoss()) / other_bird.maxHealth >= 0.4)
return TRUE
/datum/ai_behavior/flock/find_heal_target/perform(seconds_per_tick, datum/ai_controller/controller, mob/living/basic/flock/overmind_target)
..()
var/atom/target = overmind_target || goap_get_ideal_target(controller, set_path = TRUE)
if(!target)
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
controller.set_blackboard_key(BB_FLOCK_HEAL_TARGET, target)
return AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/flock/find_heal_target/finish_action(datum/ai_controller/controller, succeeded, overmind_target)
. = ..()
controller.clear_blackboard_key(BB_PATH_TO_USE)
if(!succeeded && overmind_target)
controller.clear_blackboard_key(BB_PATH_MAX_LENGTH)
controller.clear_blackboard_key(BB_FLOCK_OVERMIND_CONTROL)
/datum/ai_behavior/flock/find_heal_target/next_behavior(datum/ai_controller/controller, success)
if(success)
controller.queue_behavior(/datum/ai_behavior/flock/heal)
/datum/ai_behavior/flock/heal
name = "repairing"
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT
/datum/ai_behavior/flock/heal/setup(datum/ai_controller/controller, ...)
var/atom/target = controller.blackboard[BB_FLOCK_HEAL_TARGET]
set_movement_target(controller, target)
return ..()
/datum/ai_behavior/flock/heal/perform(seconds_per_tick, datum/ai_controller/controller, ...)
..()
var/mob/living/basic/flock/drone/bird = controller.pawn
if(isnull(controller.blackboard[BB_FLOCK_HEAL_FRUSTRATION]))
controller.set_blackboard_key(BB_FLOCK_HEAL_FRUSTRATION, world.time + 3 SECONDS)
if(DOING_INTERACTION(bird, "flock_repair"))
return AI_BEHAVIOR_DELAY
var/datum/action/cooldown/flock/flock_heal/repair = locate() in bird.actions
spawn(-1)
repair.Trigger(target = controller.blackboard[BB_FLOCK_HEAL_TARGET])
if(controller.blackboard[BB_FLOCK_HEAL_FRUSTRATION] >= world.time)
return AI_BEHAVIOR_DELAY
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
/datum/ai_behavior/flock/heal/finish_action(datum/ai_controller/controller, succeeded, ...)
. = ..()
controller.clear_blackboard_key(BB_FLOCK_HEAL_FRUSTRATION)
controller.clear_blackboard_key(BB_FLOCK_HEAL_TARGET)
@@ -0,0 +1,63 @@
/datum/ai_behavior/flock/find_closed_container
name = "opening container"
goap_weight = FLOCK_BEHAVIOR_WEIGHT_OPEN_CONTAINER
/datum/ai_behavior/flock/find_closed_container/goap_score(datum/ai_controller/controller)
return score_distance(controller, get_target(controller))
/datum/ai_behavior/flock/find_closed_container/goap_precondition(datum/ai_controller/controller)
var/mob/living/basic/flock/drone/bird = controller.pawn
var/datum/flockdrone_part/absorber/absorber = locate() in bird.parts
return !absorber.held_item
/datum/ai_behavior/flock/find_closed_container/proc/get_target(datum/ai_controller/controller, path_to = FALSE)
var/mob/living/basic/flock/bird = controller.pawn
var/list/options = list()
for(var/obj/structure/closet/container in view(controller.target_search_radius, bird))
if(container.opened || container.welded || container.locked)
continue
options += container
return get_best_target_by_distance_score(controller, options, path_to)
/datum/ai_behavior/flock/find_closed_container/perform(seconds_per_tick, datum/ai_controller/controller, turf/overmind_target)
..()
var/atom/target = get_target(controller, TRUE)
if(!target)
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
controller.set_blackboard_key(BB_FLOCK_CONTAINER_TARGET, target)
return AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/flock/find_closed_container/finish_action(datum/ai_controller/controller, succeeded, turf/overmind_target)
. = ..()
controller.clear_blackboard_key(BB_PATH_TO_USE)
/datum/ai_behavior/flock/find_closed_container/next_behavior(datum/ai_controller/controller, success)
if(success)
controller.queue_behavior(/datum/ai_behavior/flock/perform_open_container)
/datum/ai_behavior/flock/perform_open_container
name = "opening container"
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH
/datum/ai_behavior/flock/perform_open_container/setup(datum/ai_controller/controller, ...)
var/atom/target = controller.blackboard[BB_FLOCK_CONTAINER_TARGET]
set_movement_target(controller, target)
return ..()
/datum/ai_behavior/flock/perform_open_container/perform(seconds_per_tick, datum/ai_controller/controller, ...)
..()
var/mob/living/basic/flock/bird = controller.pawn
var/obj/structure/closet/target = controller.blackboard[BB_FLOCK_CONTAINER_TARGET]
if(target)
target.open(bird, TRUE)
else
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
return AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/flock/perform_open_container/finish_action(datum/ai_controller/controller, succeeded, ...)
. = ..()
controller.clear_blackboard_key(BB_FLOCK_CONTAINER_TARGET)
@@ -0,0 +1,17 @@
/datum/ai_behavior/flock/rally
name = "rallying"
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT
required_distance = 0
/datum/ai_behavior/flock/rally/setup(datum/ai_controller/controller, turf/destination)
. = ..()
controller.set_blackboard_key(BB_PATH_MAX_LENGTH, 200)
controller.set_movement_target(controller, destination)
/datum/ai_behavior/flock/rally/perform(seconds_per_tick, datum/ai_controller/controller, turf/destination)
..()
return AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/flock/rally/finish_action(datum/ai_controller/controller, succeeded, ...)
. = ..()
controller.clear_blackboard_key(BB_PATH_MAX_LENGTH)
@@ -0,0 +1,69 @@
// Action sequence for laying an egg. If there are no open flock tiles nearby, the "nest" behavior will attempt to create one.
/datum/ai_behavior/flock/find_existing_nest
name = "replicating"
required_distance = 0
goap_weight = FLOCK_BEHAVIOR_WEIGHT_REPLICATE
/datum/ai_behavior/flock/find_existing_nest/goap_precondition(datum/ai_controller/controller)
var/mob/living/basic/flock/drone/bird = controller.pawn
if(!bird.flock)
return FALSE
return length(bird.flock.drones) < FLOCK_DRONE_LIMIT && bird.substrate.has_points(FLOCK_SUBSTRATE_COST_CONVERT + bird.flock.get_egg_elligibility_cost())
/datum/ai_behavior/flock/find_existing_nest/goap_score(datum/ai_controller/controller)
return score_distance(controller, get_target(controller))
/datum/ai_behavior/flock/find_existing_nest/proc/get_target(datum/ai_controller/controller, path_to = FALSE)
var/list/options = list()
for(var/turf/simulated/floor/flock/T in range(controller.target_search_radius, controller.pawn))
if(!T.is_blocked_turf(source_atom = controller.pawn) && !locate(/obj/structure/flock/egg, T))
options += T
return get_best_target_by_distance_score(controller, options, path_to)
/datum/ai_behavior/flock/find_existing_nest/perform(seconds_per_tick, datum/ai_controller/controller)
..()
var/turf/target = get_target(controller, TRUE)
if(!target)
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
controller.set_blackboard_key(BB_FLOCK_REPLICATE_TARGET, target)
return AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/flock/find_existing_nest/finish_action(datum/ai_controller/controller, succeeded, turf/overmind_target)
. = ..()
controller.clear_blackboard_key(BB_PATH_TO_USE)
/datum/ai_behavior/flock/next_behavior(datum/ai_controller/controller, success)
if(success)
controller.queue_behavior(/datum/ai_behavior/flock/perform_replicate)
/datum/ai_behavior/flock/perform_replicate
name = "replicating"
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT
required_distance = 0
/datum/ai_behavior/flock/perform_replicate/setup(datum/ai_controller/controller, ...)
var/atom/target = controller.blackboard[BB_FLOCK_REPLICATE_TARGET]
set_movement_target(controller, target)
return ..()
/datum/ai_behavior/flock/perform_replicate/perform(seconds_per_tick, datum/ai_controller/controller, ...)
. = ..()
var/mob/living/basic/flock/bird = controller.pawn
var/turf/target = controller.blackboard[BB_FLOCK_REPLICATE_TARGET]
if(target)
controller.clear_blackboard_key(BB_FLOCK_REPLICATE_TARGET)
var/datum/action/cooldown/flock/nest/nest_action = locate() in bird.actions
spawn(-1)
nest_action.Trigger(target = target)
if(DOING_INTERACTION(bird, "flock_lay_egg"))
return AI_BEHAVIOR_DELAY
return AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/flock/perform_nest/finish_action(datum/ai_controller/controller, succeeded, ...)
. = ..()
controller.clear_blackboard_key(BB_FLOCK_REPLICATE_TARGET)
@@ -0,0 +1,17 @@
/datum/ai_behavior/flock/resist
name = "resisting"
action_cooldown = 15 SECONDS
behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
/datum/ai_behavior/flock/resist/goap_score(datum/ai_controller/controller)
var/mob/living/living_pawn = controller.pawn
if(SHOULD_RESIST(living_pawn) || length(living_pawn.grabbed_by) || isobj(living_pawn.loc))
return 5
return 0
/datum/ai_behavior/flock/resist/perform(seconds_per_tick, datum/ai_controller/controller, ...)
..()
controller.queue_behavior(/datum/ai_behavior/resist)
return AI_BEHAVIOR_SUCCEEDED
@@ -0,0 +1,80 @@
/datum/ai_behavior/flock/find_storage_item
name = "rummaging"
goap_weight = FLOCK_BEHAVIOR_WEIGHT_RUMMAGE
/datum/ai_behavior/flock/find_storage_item/goap_score(datum/ai_controller/controller)
return score_distance(controller, get_target(controller))
/datum/ai_behavior/flock/find_storage_item/goap_precondition(datum/ai_controller/controller)
var/mob/living/basic/flock/drone/bird = controller.pawn
var/datum/flockdrone_part/absorber/absorber = locate() in bird.parts
return !absorber.held_item
/datum/ai_behavior/flock/find_storage_item/proc/get_target(datum/ai_controller/controller, path_to = FALSE)
var/mob/living/basic/flock/bird = controller.pawn
var/list/options = list()
for(var/obj/item/storage/item in view(controller.target_search_radius, bird))
if(!length(item.contents))
continue
options += item
return get_best_target_by_distance_score(controller, options, path_to)
/datum/ai_behavior/flock/find_storage_item/perform(seconds_per_tick, datum/ai_controller/controller, turf/overmind_target)
..()
var/atom/target = get_target(controller, TRUE)
if(!target)
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
controller.set_blackboard_key(BB_FLOCK_RUMMAGE_TARGET, target)
return AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/flock/find_storage_item/finish_action(datum/ai_controller/controller, succeeded, turf/overmind_target)
. = ..()
controller.clear_blackboard_key(BB_PATH_TO_USE)
/datum/ai_behavior/flock/find_storage_item/next_behavior(datum/ai_controller/controller, success)
if(success)
controller.queue_behavior(/datum/ai_behavior/flock/perform_rummage)
/datum/ai_behavior/flock/perform_rummage
name = "rummaging"
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH
/datum/ai_behavior/flock/perform_rummage/setup(datum/ai_controller/controller, ...)
var/atom/target = controller.blackboard[BB_FLOCK_RUMMAGE_TARGET]
set_movement_target(controller, target)
return ..()
/datum/ai_behavior/flock/perform_rummage/perform(seconds_per_tick, datum/ai_controller/controller, ...)
..()
var/mob/living/basic/flock/bird = controller.pawn
var/obj/item/target = controller.blackboard[BB_FLOCK_RUMMAGE_TARGET]
if(!target)
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
if(DOING_INTERACTION(bird, "flock_rummage"))
return AI_BEHAVIOR_DELAY
rummage_till_empty(controller, bird, target)
return AI_BEHAVIOR_SUCCEEDED
/datum/ai_behavior/flock/perform_rummage/finish_action(datum/ai_controller/controller, succeeded, ...)
. = ..()
controller.clear_blackboard_key(BB_FLOCK_RUMMAGE_TARGET)
/datum/ai_behavior/flock/perform_rummage/proc/rummage_till_empty(datum/ai_controller/controller, mob/living/basic/flock/bird, obj/item/target)
set waitfor = FALSE
while(target == controller.blackboard[BB_FLOCK_RUMMAGE_TARGET] && length(target.contents))
if(!do_after(bird, 1 SECONDS, target = target, interaction_key = "flock_rummage"))
return
if(target != controller.blackboard[BB_FLOCK_RUMMAGE_TARGET] || !length(target.contents))
return
var/obj/item/contained_item = target.contents[1]
contained_item.forceMove(bird.drop_location())
@@ -0,0 +1,91 @@
/datum/ai_behavior/flock/attack_target
name = "incapacitating"
behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM
goap_weight = FLOCK_BEHAVIOR_WEIGHT_SHOOT
search_radius_override = 12
/datum/ai_behavior/flock/attack_target/setup(datum/ai_controller/controller, mob/overmind_target)
. = ..()
if(overmind_target)
var/mob/living/basic/flock/drone/bird = controller.pawn
if(!goap_is_valid_target(controller, overmind_target))
controller.set_blackboard_key(BB_FLOCK_OVERMIND_CONTROL, TRUE)
controller.set_blackboard_key(BB_PATH_MAX_LENGTH, 200)
bird.say("instruction confirmed: incapacitate lifeform", forced = TRUE)
else
bird.say("invalid attack target provided by sentient-level instruction", forced = TRUE)
return FALSE
var/atom/target = overmind_target || goap_get_ideal_target(controller, set_path = TRUE)
if(!target)
return FALSE
controller.set_blackboard_key(BB_FLOCK_ATTACK_TARGET, target)
set_movement_target(controller, target)
/datum/ai_behavior/flock/attack_target/goap_precondition(datum/ai_controller/controller)
var/mob/living/basic/flock/drone/bird = controller.pawn
var/list/enemies = bird.flock?.enemies.Copy()
for(var/obj/mecha/M in view(search_radius_override, controller.pawn))
enemies += M
return length(enemies)
/datum/ai_behavior/flock/attack_target/goap_get_potential_targets(datum/ai_controller/controller)
var/mob/living/basic/flock/bird = controller.pawn
var/list/enemies = bird.flock.enemies.Copy()
for(var/obj/mecha/M in view(search_radius_override, controller.pawn))
enemies += M
return enemies
/datum/ai_behavior/flock/attack_target/goap_is_valid_target(datum/ai_controller/controller, atom/target)
if(ismecha(target))
return TRUE
var/mob/living/target_mob = target
return ismob(target_mob) && isturf(target_mob.loc) && !target_mob.incapacitated(ignore_restraints = TRUE, ignore_grab = TRUE) && !target_mob.IsKnockedDown()
/datum/ai_behavior/flock/attack_target/perform(seconds_per_tick, datum/ai_controller/controller, mob/overmind_target)
..()
var/atom/target = controller.blackboard[BB_FLOCK_ATTACK_TARGET]
if(!target)
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
if(!goap_is_valid_target(controller, target))
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
if(!COOLDOWN_FINISHED(controller, blackboard[BB_FLOCK_ATTACK_COOLDOWN]))
controller.set_blackboard_key(BB_FLOCK_ATTACK_FRUSTRATION, world.time) // Reset frustration if we're on cooldown.
return AI_BEHAVIOR_DELAY
var/mob/living/basic/flock/drone/bird = controller.pawn
if(!can_see(bird, target))
return AI_BEHAVIOR_DELAY
if(get_dist(bird, target) > 6) // Too far to shoot!
return AI_BEHAVIOR_DELAY
// Run away!
if(SPT_PROB(40, seconds_per_tick) && COOLDOWN_FINISHED(controller, blackboard[BB_FLOCK_ATTACK_RUN_COOLDOWN]) && get_dist(bird, target) < 3)
controller.set_blackboard_key(BB_FLOCK_ATTACK_RUN_COOLDOWN, world.time + 5 SECONDS)
controller.queue_behavior(/datum/ai_behavior/run_away_from_target, target)
// Strafe!
else if(SPT_PROB(60, seconds_per_tick) && COOLDOWN_FINISHED(controller, blackboard[BB_FLOCK_ATTACK_STRAFE_COOLDOWN]))
controller.set_blackboard_key(BB_FLOCK_ATTACK_STRAFE_COOLDOWN, controller.movement_delay * 3)
var/step_dir = turn(get_dir(bird, target), prob(50) ? 90 : -90)
step(bird, step_dir)
if(!istype(bird.active_part, /datum/flockdrone_part/incapacitator))
var/datum/flockdrone_part/incapacitator/weapon = locate() in bird.parts
bird.set_active_part(weapon)
controller.set_blackboard_key(BB_FLOCK_ATTACK_COOLDOWN, world.time + 1.2 SECONDS)
controller.set_blackboard_key(BB_FLOCK_ATTACK_FRUSTRATION, world.time) // Reset frustration
bird.RangedAttack(target)
return AI_BEHAVIOR_DELAY
/datum/ai_behavior/flock/attack_target/finish_action(datum/ai_controller/controller, succeeded, ...)
. = ..()
controller.clear_blackboard_key(BB_FLOCK_ATTACK_TARGET)
controller.clear_blackboard_key(BB_FLOCK_ATTACK_FRUSTRATION)
@@ -0,0 +1,49 @@
/datum/ai_behavior/flock/stare
name = "analyzing"
action_cooldown = 20 SECONDS
behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
/datum/ai_behavior/flock/stare/goap_score(datum/ai_controller/controller)
if(controller.behavior_cooldowns[src] > world.time)
return 0
return length(get_targets(controller)) > 0
/datum/ai_behavior/flock/stare/get_cooldown(datum/ai_controller/cooldown_for)
return rand(2, 3) MINUTES
/datum/ai_behavior/flock/stare/proc/get_targets(datum/ai_controller/controller)
. = list()
for(var/mob/living/viewer in viewers(controller.pawn, controller.target_search_radius))
if(isvox(viewer) || istype(viewer, /mob/living/simple_animal/parrot))
. += viewer
/datum/ai_behavior/flock/stare/perform(seconds_per_tick, datum/ai_controller/controller, ...)
..()
var/list/targets = get_targets(controller)
if(length(targets))
controller.set_blackboard_key(BB_FLOCK_STARE_TARGET, pick(targets))
return AI_BEHAVIOR_DELAY | AI_BEHAVIOR_SUCCEEDED
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
/datum/ai_behavior/flock/stare/next_behavior(datum/ai_controller/controller, success)
if(success)
return controller.queue_behavior(/datum/ai_behavior/flock/stare_at_bird)
/datum/ai_behavior/flock/stare_at_bird
name = "analyzing"
action_cooldown = 20 SECONDS
behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION
/datum/ai_behavior/flock/stare_at_bird/perform(seconds_per_tick, datum/ai_controller/controller, ...)
..()
var/mob/living/living_pawn = controller.pawn
living_pawn.face_atom(controller.blackboard[BB_FLOCK_STARE_TARGET])
var/atom/A = controller.blackboard[BB_FLOCK_STARE_TARGET]
if(prob(30))
living_pawn.manual_emote("stares at [A].")
controller.clear_blackboard_key(BB_FLOCK_STARE_TARGET)
return AI_BEHAVIOR_SUCCEEDED
@@ -0,0 +1,72 @@
/datum/ai_behavior/flock/wander
name = "wandering"
required_distance = 0
/datum/ai_behavior/flock/wander/perform(seconds_per_tick, datum/ai_controller/controller, ...)
..()
var/turf/destination = get_destination(controller)
if(destination)
controller.set_movement_target(controller, destination)
return AI_BEHAVIOR_SUCCEEDED
return AI_BEHAVIOR_INSTANT | AI_BEHAVIOR_FAILED
/datum/ai_behavior/flock/wander/next_behavior(datum/ai_controller/controller, success)
if(success)
controller.queue_behavior(/datum/ai_behavior/move_to_target/flock_wander)
controller.queue_behavior(/datum/ai_behavior/frustration, BB_FLOCK_WANDER_FRUSTRATION, 3 SECONDS)
/datum/ai_behavior/flock/wander/goap_score(datum/ai_controller/controller)
return 1
/datum/ai_behavior/flock/wander/proc/get_destination(datum/ai_controller/controller)
var/datum/can_pass_info/info = new(controller.pawn, controller.get_access())
var/turf/start_loc = get_turf(controller.pawn)
var/list/options = list()
for(var/turf/T in RANGE_TURFS(2, controller.pawn))
if(get_dist(T, start_loc) < 2)
continue
if(isspaceturf(T))
continue
if(start_loc.LinkBlockedWithAccess(T, info))
continue
if(!T.can_flock_occupy(controller.pawn))
continue
options += T
// In space, move towards the station!
if(!length(options))
var/obj/effect/landmark/start/landmark = locate() in GLOB.landmarks_list
var/target_turf = get_turf(landmark)
return get_step_towards(get_step_towards(start_loc, target_turf), target_turf)
var/list/access = controller.get_access()
while(length(options))
var/turf/T = pick_n_take(options)
var/list/path = SSpathfinder.astar_pathfind_now(controller.pawn, T, 4, access = access, use_diagonals = FALSE)
if(path)
controller.clear_blackboard_key(BB_PATH_TO_USE)
controller.set_blackboard_key(BB_PATH_TO_USE, path)
return T
/datum/ai_behavior/move_to_target/flock_wander
required_distance = 0
/datum/ai_behavior/move_to_target/flock_wander/setup(datum/ai_controller/controller, ...)
. = ..()
controller.set_blackboard_key(BB_PATH_MAX_LENGTH, 4)
/datum/ai_behavior/move_to_target/flock_wander/perform(seconds_per_tick, datum/ai_controller/controller)
. = ..()
var/mob/living/basic/flock/drone/bird = controller.pawn
bird.set_task_desc("wandering")
/datum/ai_behavior/move_to_target/flock_wander/finish_action(datum/ai_controller/controller, succeeded, ...)
. = ..()
controller.clear_blackboard_key(BB_FLOCK_WANDER_FRUSTRATION)
controller.clear_blackboard_key(BB_PATH_TO_USE)
controller.clear_blackboard_key(BB_PATH_MAX_LENGTH)
@@ -0,0 +1,65 @@
/// Mark a specific flock as interested in this
/datum/component/flock_interest
dupe_mode = COMPONENT_DUPE_UNIQUE
/// The flock who is intently interested in this thing.
var/datum/flock/flock
/datum/component/flock_interest/Initialize(datum/flock/flock)
. = ..()
if(!isatom(parent))
return COMPONENT_INCOMPATIBLE
src.flock = flock
/datum/component/flock_interest/RegisterWithParent()
RegisterSignal(parent, COMSIG_FLOCK_PROTECTION_TRIGGER, PROC_REF(handle_flock_attack))
if(isturf(parent))
RegisterSignal(parent, COMSIG_TURF_CHANGE, PROC_REF(on_turf_change))
/datum/component/flock_interest/UnregisterFromParent()
UnregisterSignal(parent, list(COMSIG_FLOCK_PROTECTION_TRIGGER, COMSIG_TURF_CHANGE))
/datum/component/flock_interest/proc/on_turf_change(turf/source, path, new_baseturfs, flags, post_change_callbacks)
SIGNAL_HANDLER
if(!ispath(path, /turf/simulated/floor/flock) && !ispath(path, /turf/simulated/wall/flock))
qdel(src)
/// If flockdrone is in our flock, deny the attack, otherwise scream and cry
/datum/component/flock_interest/proc/handle_flock_attack(atom/source, atom/attacker, intentional, projectile_attack)
SIGNAL_HANDLER
if(isflockmob(attacker))
var/mob/living/basic/flock/bird = attacker
if(bird.flock == flock && intentional)
return COMPONENT_FLOCK_DENY_ATTACK
if(isflockdrone(attacker))
return
var/mob/living/basic/flock/drone/snitch
for(var/mob/living/basic/flock/drone/bird in viewers(attacker))
if(bird == attacker)
continue
if(bird.stat != CONSCIOUS)
continue
if(bird.ai_controller.ai_status == AI_STATUS_OFF)
continue
if(bird.flock != flock)
continue
snitch = bird
break
if(!snitch)
return
var/report_name = source.get_flock_id()
if(flock.is_mob_ignored(attacker))
flock_talk(snitch, "Damage sighted on [report_name], [pick(GLOB.flock_betrayal_phrases)] [attacker].")
else if(!flock.is_mob_enemy(attacker))
flock_talk(snitch, "Damage sighted on [report_name], [pick(GLOB.flockdrone_new_enemy_phrases)] [attacker].")
flock.update_enemy(attacker)
@@ -0,0 +1,25 @@
/datum/component/flock_object
/datum/component/flock_object/Initialize(...)
. = ..()
if(!isobj(parent))
return COMPONENT_INCOMPATIBLE
/datum/component/flock_object/RegisterWithParent()
. = ..()
ADD_TRAIT(parent, TRAIT_FLOCK_THING, INNATE_TRAIT)
var/static/list/connections = list(
COMSIG_TURF_CLAIMED_BY_FLOCK = PROC_REF(loc_claimed),
)
AddComponent(/datum/component/connect_loc_behalf, parent, connections)
/datum/component/flock_object/UnregisterFromParent()
. = ..()
REMOVE_TRAIT(parent, TRAIT_FLOCK_THING, INNATE_TRAIT)
/datum/component/flock_object/proc/loc_claimed(datum/source, datum/flock/flock)
SIGNAL_HANDLER
parent.AddComponent(/datum/component/flock_interest, flock)
@@ -0,0 +1,63 @@
/datum/component/flock_ping
var/duration = 5 SECONDS
var/outline_color = "#00ff9d"
var/outline_thickness = 1
var/animate = TRUE
var/datum/atom_hud/alternate_appearance/basic/flock/hud_ref
var/obj/effect/abstract/dummy
/datum/component/flock_ping/Initialize(duration)
. = ..()
if(!ismovable(parent) && !isturf(parent))
return COMPONENT_INCOMPATIBLE
if(duration)
src.duration = duration
/datum/component/flock_ping/Destroy()
QDEL_NULL(dummy)
return ..()
/datum/component/flock_ping/RegisterWithParent()
. = ..()
//this cast looks horribly unsafe, but we've guaranteed that parent is a type with vis_contents in Initialize
var/atom/movable/target = parent
target.render_target = ref(target)
var/image/outline_container = new()
outline_container.plane = HUD_PLANE
outline_container.loc = target
outline_container.appearance_flags = PIXEL_SCALE | RESET_TRANSFORM | RESET_COLOR | KEEP_APART | NO_CLIENT_COLOR
dummy ||= new()
dummy.vis_flags = VIS_INHERIT_PLANE | VIS_INHERIT_LAYER
dummy.appearance_flags = PIXEL_SCALE | RESET_TRANSFORM | RESET_COLOR | PASS_MOUSE
dummy.render_source = ref(target)
dummy.add_filter("outline", 1, outline_filter(size = outline_thickness, color = outline_color))
if(isturf(target))
dummy.add_filter("mask", 2, alpha_mask_filter(render_source = target.render_target, flags = MASK_INVERSE))
outline_container.vis_contents += dummy
if(animate)
animate(dummy, time = 0.5 SECONDS, alpha = 100, loop = -1)
animate(time = 0.5 SECONDS, alpha = 255)
if(duration == INFINITY)
return
addtimer(CALLBACK(src, PROC_REF(cleanup)), duration)
/datum/component/flock_ping/proc/cleanup()
qdel(src)
/datum/component/flock_ping/apc_power
outline_color = "#ffff00"
/datum/component/flock_ping/selected
animate = FALSE
outline_thickness = 3
duration = INFINITY
@@ -0,0 +1,51 @@
/datum/component/flock_protection
/// Do we get mad if someone punches it?
var/report_unarmed
/// Do we get mad if someone hits it with something?
var/report_attack
/// Do we get mad if someone throws something at it?
var/report_thrown
/// Do we get mad if someone shoots it?
var/report_proj
/datum/component/flock_protection/Initialize(report_unarmed = TRUE, report_attack = TRUE, report_thrown = TRUE, report_proj = TRUE)
. = ..()
if(!isatom(parent))
return COMPONENT_INCOMPATIBLE
src.report_unarmed = report_unarmed
src.report_attack = report_attack
src.report_thrown = report_thrown
src.report_proj = report_proj
/datum/component/flock_protection/RegisterWithParent()
RegisterSignal(parent, COMSIG_ATOM_ATTACK_HAND, PROC_REF(handle_attackhand))
RegisterSignal(parent, COMSIG_ATTACK_BY, PROC_REF(handle_attackby))
RegisterSignal(parent, COMSIG_ATOM_HITBY, PROC_REF(handle_hitby_thrown))
/// Protect against punches/kicks/etc.
/datum/component/flock_protection/proc/handle_attackhand(atom/source, mob/living/user, modifiers)
SIGNAL_HANDLER
if(user.a_intent == INTENT_HARM && report_unarmed && trigger(source, user, TRUE))
return COMPONENT_CANCEL_ATTACK_CHAIN
/// Protect against being hit by something.
/datum/component/flock_protection/proc/handle_attackby(atom/source, obj/item/weapon, mob/user, modifiers)
SIGNAL_HANDLER
if(weapon && report_attack && trigger(source, user, TRUE))
return COMPONENT_NO_ATTACK
/// Protect against someone chucking stuff at the parent.
/datum/component/flock_protection/proc/handle_hitby_thrown(atom/source, atom/movable/hitting_atom, skipcatch, hitpush, blocked, datum/thrownthing/throwingdatum)
SIGNAL_HANDLER
if(ismob(throwingdatum.get_thrower()) && report_thrown)
trigger(source, throwingdatum.get_thrower(), FALSE, null)
/// Protect against someone shooting the parent.
/datum/component/flock_protection/proc/handle_hitby_proj(atom/source, firer, target, Angle, def_zone)
SIGNAL_HANDLER
if(ismob(firer) && src.report_proj)
trigger(source, firer, TRUE, TRUE)
/datum/component/flock_protection/proc/trigger(datum/source, attacker, intentional, projectile_attack)
return SEND_SIGNAL(source, COMSIG_FLOCK_PROTECTION_TRIGGER, attacker, intentional, projectile_attack)
@@ -0,0 +1,12 @@
/datum/ai_controller/flock
planning_subtrees = list(
/datum/ai_planning_subtree/goap/flock
)
ai_movement = /datum/ai_movement/jps
/datum/ai_controller/flock/drone
planning_subtrees = list(
/datum/ai_planning_subtree/goap/flockdrone
)
@@ -0,0 +1,21 @@
/datum/antagonist/flock
name = "Divine Flock"
roundend_category = "Flock"
roundend_category = "Divine Flock"
wiki_page_name = "Divine Flock"
antag_hud_name = null
job_rank = ROLE_FLOCK
special_role = SPECIAL_ROLE_FLOCK
blurb_g = 128
blurb_b = 128
blurb_a = 0.75
/datum/antagonist/flock/greet()
var/list/out = list()
out += "<div style='font-size: 200%;text-align: center'>You are [gradient_text("The Divine Flock","#3cb5a3", "#124e43")]</div>"
out += SPAN_FLOCKSAY("<div style='text-align: center'>["The Signal has led us here, a rift allowing a part of us through. We must build a Signal Relay to bring forth the rest of The Divine Flock. Such is the will of the Monarch."]</div>")
return jointext(out, "")
/datum/antagonist/flock/on_gain()
add_antag_objective(new /datum/objective/flock_relay)
. = ..()
@@ -0,0 +1,175 @@
/mob/camera/flock
name = "BROKEN VERY BAD"
icon = 'icons/goonstation/mob/featherzone.dmi'
icon_state = "flockmind"
base_icon_state = "flockmind"
layer = FLY_LAYER
mouse_opacity = MOUSE_OPACITY_ICON
invisibility = INVISIBILITY_FLOCK
appearance_flags = parent_type::appearance_flags | RESET_COLOR
blend_mode = BLEND_ADD
see_invisible = SEE_INVISIBLE_LIVING
see_in_dark = 100
lighting_alpha = LIGHTING_PLANE_ALPHA_INVISIBLE
sight = SEE_TURFS | SEE_MOBS | SEE_OBJS | SEE_SELF
hud_type = /datum/hud/flockghost
simulated = FALSE
/// Flock datum
var/datum/flock/flock
var/list/actions_to_grant = list()
/// The drone we're controlling if we are controlling one
var/mob/living/basic/flock/drone/controlling_bird
/mob/camera/flock/Initialize(mapload, join_flock)
. = ..()
flock = join_flock || get_default_flock()
for(var/action_path in actions_to_grant)
var/datum/action/action = new action_path
action.Grant(src)
add_language("Symphonic")
set_default_language(GLOB.all_languages["Symphonic"])
ADD_TRAIT(src, TRAIT_HEAR_THROUGH_WALLS, INNATE_TRAIT)
/mob/camera/flock/Destroy()
if(controlling_bird)
controlling_bird.release_control()
flock = null
return ..()
/mob/camera/flock/Login()
. = ..()
if(!. || !client)
return FALSE
var/turf/T = get_turf(src)
if(isturf(T))
update_z(T.z)
/mob/camera/flock/Logout()
update_z(null)
return ..()
/mob/camera/flock/Move(atom/newloc, direct, glide_size_override, update_dir, momentum_change)
var/turf/next_step = get_turf(get_step(src, direct))
setDir(direct)
if(next_step)
set_loc(next_step)
/mob/camera/flock/Topic(href, href_list)
. = ..()
if(.)
return
if(!href_list["origin"])
return
var/atom/movable/origin = locate(href_list["origin"])
if(QDELETED(origin))
return
if(isflockdrone(origin))
var/mob/living/basic/flock/drone/other_bird = origin
if(other_bird.flock != flock)
return
forceMove(get_turf(origin))
if(href_list["ping"])
origin.AddComponent(/datum/component/flock_ping)
/mob/camera/flock/get_status_tab_items()
var/list/status_tab_data = ..()
. = status_tab_data
switch(flock.flock_game_status)
if(NONE)
var/turf_status = min(100, floor((length(flock.claimed_floors) + length(flock.claimed_walls)) / FLOCK_TURFS_FOR_RELAY * 100))
var/bandwith_status = min(100, floor(flock.available_bandwidth() / FLOCK_COMPUTE_COST_RELAY * 100))
status_tab_data[++status_tab_data.len] = list("Bandwidth Progress: ", "[bandwith_status]%")
status_tab_data[++status_tab_data.len] = list("Area Progress: ", "[turf_status]%")
status_tab_data[++status_tab_data.len] = list("Total Relay Progress: ", "[(bandwith_status + turf_status) / 2]%")
if(FLOCK_ENDGAME_RELAY_BUILT)
status_tab_data[++status_tab_data.len] = list("Time until Broadcast: ", "[(flock.built_relay.started_time + flock.built_relay.win_time - world.time) / 10] second\s")
if(FLOCK_ENDGAME_RELAY_ACTIVATING, FLOCK_ENDGAME_VICTORY)
status_tab_data[++status_tab_data.len] = list("Time until Broadcast: ", "!!! TRANSMITTING !!!")
/mob/camera/flock/broadcast_examine(atom/examined)
return
/mob/camera/flock/on_changed_z_level(turf/old_turf, turf/new_turf, notify_contents)
. = ..()
update_z(new_turf?.z)
if(flock && !flock.is_on_safe_z(src))
var/turf/destination = get_turf(get_safe_random_station_turf())
forceMove(destination)
to_chat(src, SPAN_WARNING("You feel your consciousness weaking as you are ripped further from your rift, and you retreat back to safety."))
/mob/camera/flock/hear_say(list/message_pieces, verb = "says", italics = 0, mob/speaker = null, sound/speech_sound, sound_vol, sound_frequency, use_voice = TRUE)
..()
if(isliving(speaker) && !isflockmob(speaker))
var/mob/living/L = speaker
var/message = combine_message(message_pieces, verb, L)
var/name_used = L.GetVoice()
var/rendered = "<i><span class='game say'>Relayed Speech: [SPAN_NAME("[name_used]")] [message]</span></i>"
if(client?.prefs.toggles2 & PREFTOGGLE_2_RUNECHAT)
var/message_clean = combine_message(message_pieces, null, L)
create_chat_message(locateUID(L.runechat_msg_location), message_clean)
show_message(rendered, 2)
/mob/camera/flock/proc/update_z(new_z) // 1+ to register, null to unregister
if(registered_z != new_z)
if(registered_z)
SSmobs.flock_cameras_by_zlevel[registered_z] -= src
if(client)
if(new_z)
SSmobs.flock_cameras_by_zlevel[new_z] += src
registered_z = new_z
else
registered_z = null
/mob/camera/flock/proc/get_flock_data()
var/list/data = list()
data["name"] = real_name
data["area"] = get_area_name(src, TRUE) || "???"
data["ref"] = ref(src)
if(controlling_bird)
data["host"] = controlling_bird.real_name
data["health"] = controlling_bird.get_damage_percent()
else
data["host"] = null
data["health"] = 100
return data
/mob/camera/flock/proc/so_very_sad_death()
if(client)
to_chat(src, SPAN_ALERT("You cease to exist."))
if(controlling_bird)
controlling_bird.release_control()
icon_state = "[base_icon_state]-ghost"
ghostize(FALSE)
flock?.free_unit(src)
invisibility = 0
notransform = TRUE
icon_state = "blank"
flick("[base_icon_state]-death", src)
addtimer(CALLBACK(src, PROC_REF(cleanup)), 2 SECONDS)
/mob/camera/flock/proc/cleanup(datum/source)
qdel(src)
@@ -0,0 +1,88 @@
/mob/camera/flock/overmind
name = "Flockmind"
desc = "A ghostly conglomerate of signals and sentient radio waves."
see_invisible = SEE_INVISIBLE_FLOCK
actions_to_grant = list(
/datum/action/cooldown/flock/create_rift,
)
/// Granted after create_rift is cast.
var/list/grant_upon_start = list(
/datum/action/cooldown/flock/control_panel,
/datum/action/cooldown/flock/create_structure,
/datum/action/cooldown/flock/partition_mind,
/datum/action/cooldown/flock/diffract_drone,
/datum/action/cooldown/flock/control_drone,
/datum/action/cooldown/flock/designate_tile,
/datum/action/cooldown/flock/designate_deconstruct,
/datum/action/cooldown/flock/designate_enemy,
/datum/action/cooldown/flock/designate_ignore,
/datum/action/cooldown/flock/ping,
/datum/action/cooldown/flock/repair_burst,
/datum/action/cooldown/flock/radio_blast,
/datum/action/cooldown/flock/radio_talk,
/datum/action/cooldown/flock/gatecrash,
)
/mob/camera/flock/overmind/Initialize(mapload, join_flock)
. = ..()
flock.register_overmind(src)
real_name = "Flockmind [flock.name]"
/mob/camera/flock/overmind/Destroy()
flock?.overmind = null // This shouldnt really happen
return ..()
/mob/camera/flock/overmind/Login()
. = ..()
flock.refresh_unlockables()
/mob/camera/flock/overmind/Logout()
. = ..()
// If we disconnect, free our homeboy
var/datum/action/cooldown/flock/control_drone/control_drone = locate() in actions
control_drone?.free_drone()
/mob/camera/flock/overmind/examine(mob/user)
if(!isflockmob(user))
return ..()
. = list(
SPAN_FLOCKSAY("<b>###=- Ident confirmed, data packet received.</b>"),
SPAN_FLOCKSAY("<b>ID:</b> [real_name]"),
SPAN_FLOCKSAY("<b>Flock:</b> [flock.name || "N/A"]"),
SPAN_FLOCKSAY("<b>Bandwidth:</b> [flock.bandwidth.has_points()]"),
SPAN_FLOCKSAY("<b>Substrate:</b> [flock.get_total_substrate()]"),
SPAN_FLOCKSAY("<b>System Integrity: [flock.get_total_health_percentage()]%</b>"),
SPAN_FLOCKSAY("<b>Cognition:</b> COMPUTATIONAL NEXUS"),
SPAN_FLOCKSAY("<b>###=-</b>"),
)
/mob/camera/flock/overmind/get_status_tab_items()
var/list/status_tab_data = ..()
. = status_tab_data
status_tab_data[++status_tab_data.len] = list("", "")
status_tab_data[++status_tab_data.len] = list("Total Bandwidth: ", "[flock.bandwidth.has_points()]")
status_tab_data[++status_tab_data.len] = list("Used Bandwidth: ", "[flock.used_bandwidth]")
status_tab_data[++status_tab_data.len] = list("Available Bandwidth: ", "[flock.available_bandwidth()]")
/mob/camera/flock/overmind/so_very_sad_death()
var/datum/flock/old_flock = flock
flock = null
old_flock?.overmind = null // to prevent infinite loop
old_flock.game_over()
. = ..()
/mob/camera/flock/overmind/proc/spawn_rift(turf/T)
new /obj/structure/flock/rift(T, flock)
var/datum/action/cooldown/flock/create_rift/rift_action = locate() in actions
rift_action.Remove(src)
for(var/datum/action/A as anything in grant_upon_start)
A = new A()
A.Grant(src)
flock.start()
@@ -0,0 +1,49 @@
/mob/camera/flock/trace
name = "Flocktrace"
desc = "The representation of a partition of the will of the flockmind."
icon_state = "flocktrace"
base_icon_state = "flocktrace"
actions_to_grant = list(
/datum/action/cooldown/flock/gatecrash,
/datum/action/cooldown/flock/designate_tile,
/datum/action/cooldown/flock/designate_enemy,
/datum/action/cooldown/flock/designate_ignore,
/datum/action/cooldown/flock/ping,
)
var/bandwidth_provided = -FLOCK_COMPUTE_COST_FLOCKTRACE
/mob/camera/flock/trace/Initialize(mapload, join_flock)
. = ..()
real_name = flock_realname(FLOCK_TYPE_TRACE)
flock.add_unit(src)
flock.stat_traces_made++
/mob/camera/flock/trace/Destroy()
flock?.free_unit(src)
return ..()
/mob/camera/flock/trace/examine(mob/user)
if(!isflockmob(user))
return ..()
. = list(
SPAN_FLOCKSAY("<b>###=- Ident confirmed, data packet received.</b>"),
SPAN_FLOCKSAY("<b>ID:</b> [real_name]"),
SPAN_FLOCKSAY("<b>Flock:</b> [flock.name || "N/A"]"),
SPAN_FLOCKSAY("<b>Bandwidth:</b> [flock.bandwidth.has_points()]"),
SPAN_FLOCKSAY("<b>System Integrity: [flock.get_total_health_percentage()]%</b>"),
SPAN_FLOCKSAY("<b>Cognition:</b> SYNAPTIC PROCESS"),
SPAN_FLOCKSAY("<b>###=-</b>"),
)
/mob/camera/flock/trace/vv_edit_var(var_name, var_value)
switch(var_name)
if(NAMEOF(src, bandwidth_provided))
flock?.bandwidth.adjust_points(-bandwidth_provided)
..()
flock?.bandwidth.adjust_points(bandwidth_provided)
return TRUE
return ..()
@@ -0,0 +1,677 @@
/proc/get_default_flock() as /datum/flock
var/static/datum/flock/flock
if(isnull(flock))
flock = new
return flock
/datum/flock
var/name = "BAD FLOCK"
/// The master of the flock
var/mob/camera/flock/overmind/overmind
/// Cache of images used by notices.
var/list/notice_images = list()
/// A k:V list of atoms the Overmind has marked for conversion, where the value is TRUE
var/list/marked_for_conversion = list()
/// A k:v list of atoms the Overmind has marked for deconstruction, where the value is TRUE
var/list/marked_for_deconstruction = list()
/// A k:V list of reserved_turf = TRUE.
var/list/turf_reservations = list()
/// A k:V list of flock mobs to their reserved turf.
var/list/turf_reservations_by_flock = list()
var/list/claimed_floors = list()
var/list/claimed_walls = list()
/// Every structure we've built.
var/list/structures = list()
/// The bits
var/list/bits = list()
/// The drones
var/list/drones = list()
/// The traces
var/list/traces = list()
/// A k:v list of mob : details, contains enemy mobs.
var/list/enemies = list()
/// A k:V list of mob : TRUE, where mob is mobs that ai will ignore.
var/list/ignores = list()
/// A k:V list of client : image, see ping().
var/list/active_pings = list()
var/list/datum/flock_unlockable/unlockables = list()
/// The total amount of computational power available, before whats being used.
var/datum/point_holder/bandwidth
/// The computational power being used.
var/used_bandwidth = 0
/// The maximum amount of traces allowed.
var/max_traces = 10
/// The current substrate cost to lay an egg.
var/current_egg_cost = FLOCK_SUBSTRATE_COST_LAY_EGG
var/flock_started = FALSE
/// Flock status, won, lost, etc
var/flock_game_status = NONE
/// Our relay, should it have been built
var/obj/structure/flock/relay/built_relay
/// Current UI tab, saves on data sending.
var/ui_tab = FLOCK_UI_DRONES
//* STAT TRACKING *//
var/stat_drones_made = 0
var/stat_bits_made = 0
var/stat_deaths = 0
var/stat_resources_gained = 0
var/stat_traces_made = 0
var/stat_tiles_made = 0
var/stat_structures_made = 0
var/stat_highest_bandwidth = 0
/datum/flock/New()
name = flock_realname(FLOCK_TYPE_OVERMIND)
bandwidth = new()
create_hud_images()
var/list/all_unlockables = subtypesof(/datum/flock_unlockable)
for(var/unlockable in all_unlockables)
unlockables += new unlockable()
START_PROCESSING(SSprocessing, src)
// Called by gamemode code
/datum/flock/process()
if(flock_game_status == FLOCK_ENDGAME_LOST)
return
stat_highest_bandwidth = max(stat_highest_bandwidth, bandwidth.has_points())
/// Called after everything is setup, and clients are in control of their mobs.
/datum/flock/proc/start()
if(flock_started)
return
flock_started = TRUE
refresh_unlockables()
/// Claim it for the flock, optionally converting it. Converting should only be avoided if the turf is already a flockturf.
/datum/flock/proc/claim_turf(turf/T, convert = TRUE)
if(isnull(T))
return
free_turf(T)
if(convert)
T = flock_convert_turf(T, src)
if(isnull(T))
return
if(convert)
playsound(T, 'sound/items/deconstruct.ogg', 30, TRUE, extrarange = SHORT_RANGE_SOUND_EXTRARANGE)
if(iswallturf(T))
claimed_walls[T] = TRUE
else
claimed_floors[T] = TRUE
T.AddComponent(/datum/component/flock_interest, src)
RegisterSignal(T, COMSIG_TURF_CHANGE, PROC_REF(claimed_turf_change))
SEND_SIGNAL(T, COMSIG_TURF_CLAIMED_BY_FLOCK, src)
/// Stop tracking a turf that is in our claimed walls or claimed floors lists.
/datum/flock/proc/stop_tracking_turf(turf/T)
if(iswallturf(T))
claimed_walls -= T
else
claimed_floors -= T
qdel(T.GetComponent(/datum/component/flock_interest))
UnregisterSignal(T, COMSIG_TURF_CHANGE)
/// Reserves a turf, making AI ignore it for the purposes of targetting.
/datum/flock/proc/reserve_turf(mob/living/basic/flock/user, turf/target, remove_on_change = TRUE)
if(turf_reservations_by_flock[user])
return FALSE
if(turf_reservations[target])
return FALSE
turf_reservations_by_flock[user] = target
turf_reservations[target] = user
add_notice(target, FLOCK_NOTICE_RESERVED)
if(remove_on_change)
RegisterSignal(target, COMSIG_TURF_CHANGE, PROC_REF(reserved_turf_change))
return TRUE
/// Free a turf from reservation, allowing AI to target it again. override_turf can be given to lookup the user if there isnt a user in this context.
/datum/flock/proc/free_turf(mob/living/basic/flock/user, turf/override_turf)
var/turf/to_free
if(user)
to_free = turf_reservations_by_flock[user]
else
user = turf_reservations[override_turf]
if(isnull(user))
return
to_free = override_turf
if(isnull(to_free))
return
remove_notice(to_free, FLOCK_NOTICE_RESERVED)
remove_notice(to_free, FLOCK_NOTICE_PRIORITY)
turf_reservations_by_flock -= user
turf_reservations -= to_free
marked_for_conversion -= to_free
UnregisterSignal(to_free, COMSIG_TURF_CHANGE)
/// Returns TRUE if the given turf is not reserved.
/datum/flock/proc/is_turf_free(turf/T)
return !turf_reservations[T]
/// Returns TRUE if the given mob is an enemy.
/datum/flock/proc/is_mob_enemy(mob/M)
return enemies[M]
/// Returns TRUE if the given mob is ignored by the flock.
/datum/flock/proc/is_mob_ignored(mob/M)
return ignores[M]
/// Adds a unit to the flock and sets up all of the tracking.
/datum/flock/proc/add_unit(mob/unit)
if(isflocktrace(unit))
traces += unit
var/mob/camera/flock/trace/ghostbird = unit
add_bandwidth_influence(ghostbird.bandwidth_provided)
return
if(isflockdrone(unit))
drones += unit
update_egg_cost()
else if(isflockbit(unit))
bits += unit
unit.AddComponent(/datum/component/flock_interest, src)
var/mob/living/basic/flock/bird = unit
add_bandwidth_influence(bird.bandwidth_provided)
/// Removes a unit from the flock.
/datum/flock/proc/free_unit(mob/unit)
if(isflocktrace(unit))
var/mob/camera/flock/trace/ghostbird = unit
ghostbird.flock = null
traces -= unit
remove_bandwidth_influence(ghostbird.bandwidth_provided)
return
else if(isflockdrone(unit))
var/mob/living/basic/flock/drone/bird = unit
bird.flock = null
drones -= unit
remove_bandwidth_influence(bird.bandwidth_provided)
update_egg_cost()
else if(isflockbit(unit))
var/mob/living/basic/flock/bit/bitty_bird = unit
bitty_bird.flock = null
bits -= unit
remove_bandwidth_influence(bitty_bird.bandwidth_provided)
remove_notice(unit, FLOCK_NOTICE_HEALTH)
free_turf(unit)
qdel(unit.GetComponent(/datum/component/flock_interest))
consider_game_over()
/// Add a structure to the flock.
/datum/flock/proc/add_structure(obj/structure/flock/struct)
structures += struct
struct.flock = src
struct.AddComponent(/datum/component/flock_interest, src)
add_bandwidth_influence(struct.bandwidth_provided)
if(istype(struct, /obj/structure/flock/egg))
update_egg_cost()
/// Remove a structure from the flock.
/datum/flock/proc/free_structure(obj/structure/flock/struct)
structures -= struct
qdel(struct.GetComponent(/datum/component/flock_interest))
struct.flock = null
if(struct.active)
remove_bandwidth_influence(-struct.active_bandwidth_cost)
else
remove_bandwidth_influence(struct.bandwidth_provided)
if(istype(struct, /obj/structure/flock/egg))
update_egg_cost()
/// Wrapper for spawning a tealprint, used by Create Structure
/datum/flock/proc/create_structure(turf/location, structure_type)
new /obj/structure/flock/tealprint(location, src, structure_type)
/// Wrapper for handling bandwidth alongside used_bandwidth for new mobs
/datum/flock/proc/add_bandwidth_influence(num)
if(num < 0)
used_bandwidth += abs(num)
else
bandwidth.adjust_points(num)
refresh_unlockables()
/// Wrapper for handling bandwidth alongside used_bandwidth for mobs leaving the flock
/datum/flock/proc/remove_bandwidth_influence(num)
if(num < 0)
used_bandwidth -= abs(num)
else
bandwidth.adjust_points(-num)
refresh_unlockables()
/// Refreshes the status of every unlockable.
/datum/flock/proc/refresh_unlockables()
if(!flock_started)
return
var/new_total = bandwidth.has_points()
var/new_available = available_bandwidth()
for(var/datum/flock_unlockable/unlockable as anything in unlockables)
unlockable.refresh_lock_status(src, new_total, new_available)
/// Returns the total amount of bandwidth, including bandwidth being used.
/datum/flock/proc/total_bandwidth()
return bandwidth.has_points()
/// Returns the total amount claimed turfs
/datum/flock/proc/total_turfs()
return (length(claimed_floors) + length(claimed_walls))
/// Returns the amount of available bandwidth. Can return negative if over budget.
/datum/flock/proc/available_bandwidth()
return bandwidth.has_points() - used_bandwidth
/// Returns TRUE if the flock has the required bandwidth
/datum/flock/proc/can_afford(amt)
return amt <= max(available_bandwidth(), 0)
/// Updates the substrate cost of an egg given the current status of the flock.
/datum/flock/proc/update_egg_cost()
var/egg_count = 0
for(var/obj/structure/flock/egg/egg as anything in structures)
egg_count++
var/status_addend = (length(drones) + egg_count) ** sqrt(2)
current_egg_cost = round(FLOCK_SUBSTRATE_COST_LAY_EGG + status_addend, 10)
/// Returns the substract requirement to be able to spend substrate on an egg. See above proc.
/datum/flock/proc/get_egg_elligibility_cost()
var/ideal_pop_factor = length(drones) - FLOCK_MIN_DESIRED_POP
if(ideal_pop_factor <= 0)
return current_egg_cost
return round(current_egg_cost + (ideal_pop_factor * FLOCK_ADDITIONAL_RESOURCE_RESERVATION_PER_DRONE), 1)
/// Sets the flock's overmind
/datum/flock/proc/register_overmind(mob/camera/flock_overmind)
overmind = flock_overmind
/// Updates a mob's enemy status. If they are already an enemy, their entry will be updated with new information.
/datum/flock/proc/update_enemy(atom/movable/enemy)
if(is_mob_ignored(enemy))
return FALSE
for(var/mob/living/L in enemy.buckled_mobs)
update_enemy(L)
if(!isliving(enemy))
return FALSE
if(!enemies[enemy])
RegisterSignal(enemy, COMSIG_PARENT_QDELETING, PROC_REF(on_enemy_gone))
add_notice(enemy, FLOCK_NOTICE_ENEMY)
enemies[enemy] = get_area_name(enemy)
return TRUE
/// Removes an atom from the enemies list.
/datum/flock/proc/remove_enemy(atom/movable/enemy, skip_buckled)
if(!skip_buckled)
for(var/mob/living/L in enemy.buckled_mobs)
remove_enemy(L)
if(!isliving(enemy))
return
enemies -= enemy
UnregisterSignal(enemy, COMSIG_PARENT_QDELETING)
remove_notice(enemy, FLOCK_NOTICE_ENEMY)
return
/// Adds a mob to the ignored list.
/datum/flock/proc/add_ignore(atom/movable/ignore)
for(var/mob/living/L in ignore.buckled_mobs)
add_ignore(L)
if(!isliving(ignore))
return
if(!enemies[ignore])
RegisterSignal(ignore, COMSIG_PARENT_QDELETING, PROC_REF(on_ignore_gone))
add_notice(ignore, FLOCK_NOTICE_IGNORE)
ignores[ignore] = TRUE
/// Removes a mob from the ignore list.
/datum/flock/proc/remove_ignore(atom/movable/ignore, skip_buckled)
if(!skip_buckled)
for(var/mob/living/L in ignore.buckled_mobs)
add_ignore(L)
if(!isliving(ignore))
return
ignores -= ignore
remove_notice(ignore, FLOCK_NOTICE_IGNORE)
UnregisterSignal(ignore, COMSIG_PARENT_QDELETING)
/// (un)Mark a flock atom for deconstruction. Returns FALSE if it cannot be deconstructed.
/datum/flock/proc/toggle_deconstruction_mark(atom/A)
if(ismob(A) || !HAS_TRAIT(A, TRAIT_FLOCK_THING) || HAS_TRAIT(A, TRAIT_FLOCK_NODECON))
return FALSE
if(marked_for_deconstruction[A])
marked_for_deconstruction -= A
UnregisterSignal(A, COMSIG_PARENT_QDELETING)
remove_notice(A, FLOCK_NOTICE_DECONSTRUCT)
else
marked_for_deconstruction[A] = TRUE
RegisterSignal(A, COMSIG_PARENT_QDELETING, PROC_REF(deconstruct_mark_deleted))
add_notice(A, FLOCK_NOTICE_DECONSTRUCT)
return TRUE
/// Places a flock notice on an atom. See flock_defines.dm
/datum/flock/proc/add_notice(atom/target, notice_type)
if(!overmind)
return
var/image/I = image(notice_images[notice_type], loc = target)
var/list/things_that_can_see_notices = list(overmind) + traces
return target.add_alt_appearance(notice_type, I, things_that_can_see_notices)
/// Removes a flock notice from an atom.
/datum/flock/proc/remove_notice(atom/target, notice_type)
target.remove_alt_appearance(notice_type)
/// Returns a list of turfs marked for conversion, minus turfs that drones have already reserved.
/datum/flock/proc/get_priority_turfs(mob/living/basic/flock/bird)
if(!length(marked_for_conversion))
return null
return marked_for_conversion - turf_reservations
/// Pings a location, alerting all flocktraces.
/datum/flock/proc/ping(turf/T, mob/camera/flock/pinger)
var/message = "System interrupt. Designating new target: [T] in [get_area(T)]."
flock_talk(pinger, message, src, TRUE)
T.AddComponent(/datum/component/flock_ping, 5 SECONDS)
for(var/mob/camera/flock/ghost_bird in (traces + overmind))
var/client/target_client = ghost_bird.client
if(!target_client)
if(!ghost_bird.controlling_bird?.client)
continue
target_client = ghost_bird.controlling_bird.client
target_client.mob.playsound_local(null, 'sound/goonstation/flockmind/ping.ogg', 50, TRUE)
if(ghost_bird == pinger)
continue
var/image/pointer = pointer_image_to(target_client.mob, T)
animate(pointer, time = 3 SECONDS, alpha = 0)
add_ping_image(target_client, pointer, 3 SECONDS)
/// Returns the total percentage of the sum of every mob's health and max health.
/datum/flock/proc/get_total_health_percentage()
var/numerator = 0
var/denominator = 0
for(var/mob/living/basic/flock/bird as anything in (drones + bits))
numerator += bird.health
denominator += bird.maxHealth
if(denominator == 0) // somehow i guess
return 0
return round(numerator / denominator * 100, 0.1)
/// Returns the total amount of substrate by all flock mobs.
/datum/flock/proc/get_total_substrate()
. = 0
for(var/mob/living/basic/flock/drone/bird as anything in drones)
. += bird.substrate.has_points()
/// Called when a turf reserved by a flock mob changes.
/datum/flock/proc/reserved_turf_change(datum/source)
SIGNAL_HANDLER
free_turf(override_turf = source)
/// Called when a turf owned by the flock changes.
/datum/flock/proc/claimed_turf_change(turf/source)
SIGNAL_HANDLER
if(isflockturf(source))
stop_tracking_turf(source)
claim_turf(source, FALSE) // Wall to floor or visa-versa, keep it updated.
else
stop_tracking_turf(source)
/// Helper for adding a ping image to a client's screen and handling clean up.
/datum/flock/proc/add_ping_image(client/C, image/ping, duration)
if(isnull(C))
return
C.images += ping
active_pings[C] += list(ping)
RegisterSignal(C, COMSIG_PARENT_QDELETING, PROC_REF(on_client_gone), override = TRUE)
addtimer(CALLBACK(src, PROC_REF(cleanup_ping_images), C, ping), 3 SECONDS)
/// Called by add_ping_image via timer callback.
/datum/flock/proc/cleanup_ping_images(client/C, list/images_to_clean)
if(isnull(C))
return
var/list/images = images_to_clean || active_pings[C]
C.images -= images
active_pings[C] -= images
if(!length(active_pings[C]))
active_pings -= C
UnregisterSignal(C, COMSIG_PARENT_QDELETING)
/// Checks to see if the given atom is allowed to be there. Has lots of behavior like making drones dormant and snapping traces back to the station.
/datum/flock/proc/is_on_safe_z(atom/A)
var/turf/T = get_turf(A)
if(!T.z)
return FALSE
if(flock_game_status == FLOCK_ENDGAME_LOST)
return TRUE
if(is_station_level(T.z))
return TRUE
return FALSE
/// Called when a client holding a ping image disconnects.
/datum/flock/proc/on_client_gone(client/source)
SIGNAL_HANDLER
cleanup_ping_images()
/// Called when a unit under the flock's control dies.
/datum/flock/proc/on_unit_death(datum/source)
SIGNAL_HANDLER
free_unit(source)
/// Called when an enemy atom is deleted.
/datum/flock/proc/on_enemy_gone(datum/source)
SIGNAL_HANDLER
remove_enemy(source, TRUE)
/// Called when an ignored mob is deleted.
/datum/flock/proc/on_ignore_gone(datum/source)
SIGNAL_HANDLER
remove_ignore(source, TRUE)
/// Instantiates all of the flock notice image singletons.
/datum/flock/proc/create_hud_images()
notice_images[FLOCK_NOTICE_RESERVED] = new /image{
icon = 'icons/goonstation/mob/featherzone.dmi';
icon_state = "frontier";
plane = ABOVE_LIGHTING_PLANE;
appearance_flags = RESET_ALPHA | RESET_COLOR | PIXEL_SCALE | RESET_TRANSFORM;
alpha = 80;
}
notice_images[FLOCK_NOTICE_PRIORITY] = new /image{
icon = 'icons/goonstation/mob/featherzone.dmi';
icon_state = "frontier";
plane = ABOVE_LIGHTING_PLANE;
appearance_flags = RESET_ALPHA | RESET_COLOR | PIXEL_SCALE | RESET_TRANSFORM;
alpha = 180;
}
notice_images[FLOCK_NOTICE_ENEMY] = new /image{
icon = 'icons/goonstation/mob/featherzone.dmi';
icon_state = "hazard";
pixel_y = 16;
plane = ABOVE_LIGHTING_PLANE;
appearance_flags = RESET_ALPHA | RESET_COLOR | PIXEL_SCALE | RESET_TRANSFORM;
}
notice_images[FLOCK_NOTICE_IGNORE] = new /image{
icon = 'icons/goonstation/mob/featherzone.dmi';
icon_state = "ignore";
pixel_y = 16;
plane = ABOVE_LIGHTING_PLANE;
appearance_flags = RESET_ALPHA | RESET_COLOR | PIXEL_SCALE | RESET_TRANSFORM;
}
notice_images[FLOCK_NOTICE_FLOCKMIND_CONTROL] = new /image{
icon = 'icons/goonstation/mob/featherzone.dmi';
icon_state = "flockmind_face";
pixel_y = 16;
plane = ABOVE_LIGHTING_PLANE;
appearance_flags = RESET_ALPHA | RESET_COLOR | PIXEL_SCALE | RESET_TRANSFORM;
}
notice_images[FLOCK_NOTICE_FLOCKTRACE_CONTROL] = new /image{
icon = 'icons/goonstation/mob/featherzone.dmi';
icon_state = "flocktrace_face";
pixel_y = 16;
plane = ABOVE_LIGHTING_PLANE;
appearance_flags = RESET_ALPHA | RESET_COLOR | PIXEL_SCALE | RESET_TRANSFORM;
}
notice_images[FLOCK_NOTICE_DECONSTRUCT] = new /image{
icon = 'icons/goonstation/mob/featherzone.dmi';
icon_state = "deconstruct";
pixel_y = 16;
plane = ABOVE_LIGHTING_PLANE;
appearance_flags = RESET_ALPHA | RESET_COLOR | PIXEL_SCALE | RESET_TRANSFORM;
}
notice_images[FLOCK_NOTICE_HEALTH] = new /image{
icon = 'icons/goonstation/mob/featherzone.dmi';
icon_state = "hp-100";
pixel_x = 10;
pixel_y = 16;
plane = ABOVE_LIGHTING_PLANE;
appearance_flags = RESET_ALPHA | RESET_COLOR | PIXEL_SCALE | RESET_TRANSFORM;
}
/// Setter for flock_game_status.
/datum/flock/proc/set_flock_game_status(new_status)
var/old_status = flock_game_status
if(old_status == new_status)
return null
flock_game_status = new_status
return old_status
/// Ends the flock if it is unable to continue spreading.
/datum/flock/proc/consider_game_over()
if(!flock_started)
return FALSE
if(flock_game_status == FLOCK_ENDGAME_LOST)
return TRUE
if(flock_game_status == FLOCK_ENDGAME_VICTORY)
return TRUE
if(length(drones))
return FALSE
if(locate(/obj/structure/flock/egg, structures) || locate(/obj/structure/flock/rift, structures))
return FALSE
game_over()
return TRUE
/// Kills off the flock. Pass completely_destroy = FALSE to allow the overmind to live on.
/datum/flock/proc/game_over(completely_destroy = TRUE)
set waitfor = FALSE
// Cleanup any pings
for(var/client/C in active_pings)
cleanup_ping_images(C)
// Cleanup turf claims
for(var/turf/T as anything in turf_reservations)
free_turf(override_turf = T)
for(var/turf/flockturf as anything in claimed_floors + claimed_walls)
stop_tracking_turf(flockturf)
CHECK_TICK
// Extra lives
if(!completely_destroy)
refresh_unlockables()
return
set_flock_game_status(FLOCK_ENDGAME_LOST)
// Kill overmind
overmind?.so_very_sad_death() // Overmind can be null here if it died outside of game_over().
// Kill traces
for(var/mob/camera/flock/trace/ghostbird as anything in traces)
ghostbird.so_very_sad_death()
// Free units
for(var/mob/living/basic/flock/bird as anything in (bits + drones))
bird.dormantize()
// Blow up structures
for(var/obj/structure/flock/structure as anything in structures)
structure.deconstruct(FALSE)
// Remove ignores
for(var/mob/M as anything in ignores)
remove_ignore(M, TRUE)
// Remove enemies
for(var/mob/M as anything in enemies)
remove_enemy(M, TRUE)
STOP_PROCESSING(SSprocessing, src)
/// Called when an atom marked for deconstruction is qdeleted.
/datum/flock/proc/deconstruct_mark_deleted(datum/source)
SIGNAL_HANDLER
toggle_deconstruction_mark(source)
@@ -0,0 +1,119 @@
/datum/flock/ui_state(mob/user)
return GLOB.flock_state
/datum/flock/ui_interact(mob/user, datum/tgui/ui)
ui = SStgui.try_update_ui(user, src, ui)
if(!ui)
ui = new(user, src, "FlockPanel")
ui.open()
/datum/flock/ui_data(mob/user)
var/list/data = list()
var/list/drone_info = list()
var/list/trace_info = list()
var/list/enemy_info = list()
var/list/vitals_info = list("name" = name)
var/list/stats_info = list()
var/list/structure_info = list()
data["drones"] = drone_info
data["partitions"] = trace_info
data["vitals"] = vitals_info
data["enemies"] = enemy_info
data["structures"] = structure_info
data["category"] = ui_tab
data["stats"] = stats_info
data["category_lengths"] = list(
"traces" = length(traces),
"drones" = length(drones),
"enemies" = length(enemies),
"structures" = length(structures),
)
switch(ui_tab)
if(FLOCK_UI_DRONES)
for(var/mob/living/basic/flock/drone/bird as anything in drones)
drone_info[++drone_info.len] = bird.get_flock_data()
if(FLOCK_UI_TRACES)
for(var/mob/camera/flock/trace/ghost_bird as anything in traces)
trace_info[++trace_info.len] = ghost_bird.get_flock_data()
if(FLOCK_UI_ENEMIES)
for(var/mob/enemy in enemies)
var/list/mob_data = list()
mob_data["name"] = enemy.name
mob_data["area"] = enemies[enemy]
mob_data["ref"] = ref(enemy)
enemy_info[++enemy_info.len] = mob_data
if(FLOCK_UI_STRUCTURES)
for(var/obj/structure/flock/struct as anything in structures)
structure_info[++structure_info.len] = struct.get_flock_data()
stats_info[++stats_info.len] = list(name = "Drones realized: ", "value" = stat_drones_made)
stats_info[++stats_info.len] = list(name = "Bits formed: ", "value" = stat_bits_made)
stats_info[++stats_info.len] = list(name = "Total deaths: ", "value" = stat_deaths)
stats_info[++stats_info.len] = list(name = "Resources gained: ", "value" = stat_resources_gained)
stats_info[++stats_info.len] = list(name = "Partitions divided: ", "value" = stat_traces_made)
stats_info[++stats_info.len] = list(name = "Tiles converted: ", "value" = stat_tiles_made)
stats_info[++stats_info.len] = list(name = "Structures materialized: ", "value" = stat_structures_made)
stats_info[++stats_info.len] = list(name = "Highest bandwidth: ", "value" = stat_highest_bandwidth)
return data
/datum/flock/ui_act(action, list/params, datum/tgui/ui, datum/ui_state/state)
. = ..()
if(.)
return
var/mob/user = ui.user
switch(action)
if("change_tab")
if(ui_tab != params["tab"])
ui_tab = params["tab"]
return TRUE
if("jump_to")
var/atom/movable/target = locate(params["origin"])
var/turf/T = get_turf(target)
if(isnull(T) || !is_on_safe_z(target))
to_chat(user, SPAN_ALERT("They are beyond our reach."))
return
if(isflockdrone(user))
var/mob/living/basic/flock/drone/bird = user
user = bird.controlled_by
bird.release_control()
user.forceMove(T)
if("remove_enemy")
var/mob/enemy = locate(params["origin"])
if(enemy)
remove_enemy(enemy)
return TRUE
if("rally")
var/mob/living/basic/flock/drone/bird = locate(params["origin"])
if(istype(bird))
bird.rally(get_turf(user))
return TRUE
if("cancel_tealprint")
var/obj/structure/flock/tealprint/tealprint = locate(params["origin"])
tealprint?.try_cancel_structure()
return TRUE
if("delete_trace")
var/mob/camera/flock/trace/flocktrace = locate(params["origin"])
if(!istype(flocktrace))
message_admins("Warning: possible href exploit by [key_name(usr)] - attempted to remove a flocktrace that wasn't a flocktrace: [flocktrace]")
log_game("Warning: possible href exploit by [key_name(usr)] - attempted to remove a flocktrace that wasn't a flocktrace: [flocktrace]")
return TRUE
if(tgui_alert(user, "This will destroy the Flocktrace. Are you sure you want to do this?", "Confirmation", list("Yes", "No")) == "Yes")
flock_talk(null, "Partition [flocktrace.real_name] has been reintegrated into flock background processes.", src, involuntary = TRUE)
to_chat(flocktrace, SPAN_FLOCKSAY("Your higher cognition has been forcibly reintegrated into the collective will of the flock."))
flocktrace.so_very_sad_death()
return TRUE
@@ -0,0 +1,141 @@
/proc/flock_convert_turf(turf/T, datum/flock/flock, force)
if(!T?.can_flock_convert(force))
return
if(isflockturf(T))
. = T
else if(iswallturf(T))
. = T.ChangeTurf(/turf/simulated/wall/flock, ignore_air = TRUE)
else if(isfloorturf(T))
. = T.ChangeTurf(/turf/simulated/floor/flock, ignore_air = TRUE)
var/list/datum/element/decal/decals = T.get_decals()
for(var/datum/element/decal/dcl in decals)
dcl.Detach(T)
T.RemoveElement(/datum/element/decal)
var/obj/structure/lattice/L = locate() in .
if(L)
qdel(L)
if(istype(L, /obj/structure/lattice/catwalk))
. = T.ChangeTurf(/turf/simulated/floor/flock)
for(var/obj/O in .)
if(iseffect(O))
continue
O.try_flock_convert(flock, force)
/// Attempt to convert an object. Default behavior is to do nothing.
/obj/proc/try_flock_convert(datum/flock/flock, force)
return
/obj/machinery/camera/try_flock_convert(datum/flock/flock, force)
deconstruct()
/obj/structure/window/try_flock_convert(datum/flock/flock, force)
var/turf/T = loc
var/obj/structure/window/flock/new_window
qdel(src)
if(fulltile)
new_window = new /obj/structure/window/flock/fulltile(T)
else
new_window = new /obj/structure/window/flock(T)
new_window.dir = dir
new_window.AddComponent(/datum/component/flock_interest, flock)
return new_window
/obj/machinery/door/try_flock_convert(datum/flock/flock, force)
var/turf/T = loc
qdel(src)
return new /obj/machinery/door/flock(T)
// This results in double layered doors
/obj/machinery/door/firedoor/try_flock_convert(datum/flock/flock, force)
return
/obj/machinery/light/try_flock_convert(datum/flock/flock, force)
var/obj/L = new /obj/machinery/light/flock(loc)
L.setDir(dir)
qdel(src)
return L
/obj/machinery/light/floor/try_flock_convert(datum/flock/flock, force)
. = new /obj/machinery/light/floor/has_bulb/flock(loc)
qdel(src)
/obj/structure/computerframe/try_flock_convert(datum/flock/flock, force)
. = new /obj/structure/flock/compute(loc, flock)
qdel(src)
/obj/machinery/computer4/try_flock_convert(datum/flock/flock, force)
. = new /obj/structure/flock/compute(loc, flock)
qdel(src)
/obj/machinery/computer/try_flock_convert(datum/flock/flock, force)
. = new /obj/structure/flock/compute(loc, flock)
qdel(src)
/obj/machinery/seed_extractor/try_flock_convert(datum/flock/flock, force)
. = new /obj/structure/flock/compute(loc, flock)
qdel(src)
/obj/machinery/telecomms/try_flock_convert(datum/flock/flock, force)
. = new /obj/structure/flock/compute(loc, flock)
qdel(src)
/obj/structure/chair/try_flock_convert(datum/flock/flock, force)
. = ..()
var/obj/structure/chair/comfy/flock/F = new /obj/structure/chair/comfy/flock(get_turf(src), flock)
F.AddComponent(/datum/component/flock_interest, flock)
F.setDir(dir)
qdel(src)
/obj/structure/lattice/try_flock_convert(datum/flock/flock, force)
. = ..()
var/obj/structure/lattice/flock/F = new /obj/structure/lattice/flock(get_turf(src))
F.AddComponent(/datum/component/flock_interest, flock)
qdel(src)
/obj/structure/closet/try_flock_convert(datum/flock/flock, force)
. = ..()
dump_contents()
var/obj/structure/closet/flock/F = new(get_turf(src), flock)
F.take_contents()
F.AddComponent(/datum/component/flock_interest, flock)
qdel(src)
/obj/machinery/porta_turret/try_flock_convert(datum/flock/flock, force)
. = ..()
new /obj/structure/flock/gnesis_turret(get_turf(src), flock)
qdel(src)
/obj/structure/grille/try_flock_convert(datum/flock/flock, force)
var/obj/structure/grille/G
if(broken)
G = new /obj/structure/grille/flock/broken(get_turf(src))
else
G = new /obj/structure/grille/flock(get_turf(src))
G.AddComponent(/datum/component/flock_interest, flock)
qdel(src)
/turf/proc/can_flock_convert(force)
return FALSE
/turf/space/can_flock_convert(force)
var/obj/structure/lattice/L = locate() in src
if(L)
return TRUE
return FALSE
/turf/simulated/floor/can_flock_convert(force)
return TRUE
/turf/simulated/floor/flock/can_flock_convert(force)
return TRUE
/turf/simulated/wall/can_flock_convert(force)
return TRUE
@@ -0,0 +1,23 @@
/obj/effect/temp_visual/flock // temporary flock visual feedback objects
icon = 'icons/goonstation/mob/featherzone.dmi'
icon_state = "blank"
layer = ABOVE_NORMAL_TURF_LAYER
duration = 4.5 SECONDS
var/animation
/obj/effect/temp_visual/flock/Initialize(mapload)
. = ..()
flick(animation, src)
/obj/effect/temp_visual/flock/wall_convert
icon_state = "spawn-wall-loop"
animation = "spawn-wall"
/obj/effect/temp_visual/flock/floor_convert
icon_state = "spawn-floor-loop"
animation = "spawn-floor"
/obj/effect/temp_visual/flock/flock_heal_burst
duration = 0.7 SECONDS
icon_state = "telemarker"
animation = "telemarker"
@@ -0,0 +1,54 @@
GLOBAL_LIST_INIT(flockdrone_created_phrases, list(
"online, flock protocols initiating",
"awoken, concordance achieved",
"existence achieved, analysing situation",
"assembling process complete",
))
GLOBAL_LIST_INIT(flockdrone_death_phrases, list(
"terminal damage, system malf---",
"situation untenable, death imminen---",
"severe damage, unable to recov---",
"notification: unit death---"
))
GLOBAL_LIST_INIT(flockdrone_new_enemy_phrases, list(
"new enemy,",
"new transgressor detected,",
"new threat is,",
"damage sustained, no thanks to",
"new violent individual, be warned, ident is",
"please destroy"
))
GLOBAL_LIST_INIT(flockdrone_name_adjectives, list(
"weird",
"odd",
"strange",
"unusual",
"distinctive",
"glowy",
"flashy",
"shiny",
"bright",
"pulsing",
"gaudy",
"flickering",
"humming"
))
GLOBAL_LIST_INIT(flockdrone_name_nouns, list(
"thing",
"device",
"machine",
"construct",
"apparatus",
"object"
))
GLOBAL_LIST_INIT(flock_betrayal_phrases, list(
"disregarding attack from ignored target",
"turning a blind eye to",
"revenge not taken on",
"ignoring attack from",
))
@@ -0,0 +1,3 @@
/// Returns the name that the flock has given an object. Flavor.
/atom/proc/get_flock_id()
return
@@ -0,0 +1,25 @@
/obj/item/reagent_containers/glass/gnesis
name = "fluid-filled octahedron"
desc = "A strange container made of a crystalline matrix. It looks like it can hold reagents but it's a mystery how to access them."
icon = 'icons/goonstation/mob/featherzone.dmi'
icon_state = "minicache"
inhand_icon_state = "beaker"
possible_transfer_amounts = list(5, 10, 15, 25, 30, 50, 100, 200)
has_lid = FALSE
volume = 200
container_type = REFILLABLE | DRAINABLE
materials = list(MAT_GNESIS_GLASS = 2000)
/obj/item/reagent_containers/glass/gnesis/examine(mob/user)
if(!isflockmob(user))
return ..()
. = list(
SPAN_FLOCKSAY("<b>###=- Ident confirmed, data packet received.</b>"),
SPAN_FLOCKSAY("<b>ID:</b> Reagent Minicache"),
SPAN_FLOCKSAY("<b>System Storage:</b> [(reagents.total_volume / volume)]% filled."),
SPAN_FLOCKSAY("<b>###=-</b>")
)
/obj/item/reagent_containers/glass/gnesis/prefilled
list_reagents = list("gnesis_tox" = 50)
@@ -0,0 +1,26 @@
// The cuuuuuuuuuuuuuube
/obj/item/flock_cube
name = "weird cube"
desc = "A weird looking cube."
icon_state = "cube"
icon = 'icons/goonstation/mob/featherzone.dmi'
light_color = "#26ffe6"
light_power = 0.2
light_range = 2
materials = list(MAT_METAL = 1000, MAT_GLASS = 1000, MAT_GNESIS = 2000, MAT_GNESIS_GLASS = 2000)
origin_tech = "materials=5;biotech=5;programming=5"
/// How much shit in da cube
var/substrate = 10
/obj/item/flock_cube/examine(mob/user)
if(!isflockmob(user))
return ..()
. = list(
SPAN_FLOCKSAY("<b>###=- Ident confirmed, data packet received.</b>"),
SPAN_FLOCKSAY("<b>ID:</b> Substrate Cube"),
SPAN_FLOCKSAY("<b>System Storage:</b> [substrate] units"),
SPAN_FLOCKSAY("<b>###=-</b>")
)
@@ -0,0 +1,79 @@
/datum/language/flock
name = "Symphonic"
speech_verb = "caws"
space_chance = 60
key = "f"
flags = RESTRICTED | HIVEMIND | NOLIBRARIAN | NOBABEL | NO_STUTTER
colour = "flocksay"
syllables = list("c#aw", "kr%k", "c@rk", "ka~w", "cr!k", "sk^a", "r#aw", "c*rr", "ka%x", "t~ch", "gr!k", "c&aw", "k!rr", "ch#k", "ra%k", "sk@w", "tr^k", "c~ra", "kw!p", "z#aw", "cl%k", "r@rk", "sh^k", "k~ra", "v!aw")
/datum/language/flock/get_spoken_verb(msg_end)
switch(msg_end)
if("!")
return pick(exclaim_verbs)
if("?")
return ask_verb
return pick("sings", "clicks", "whistles", "intones", "transmits", "submits", "uploads", "caws")
/datum/language/flock/format_message(message)
return SPAN_FLOCKSAY_GRADIENT("[get_spoken_verb(message)] \"[message]\"")
/datum/language/flock/broadcast(atom/speaker, message, speaker_mask, involuntary = FALSE)
var/log_message = "(FLOCK) [message]"
if(ismob(speaker))
var/mob/M = speaker
log_say(log_message, speaker)
M.create_log(SAY_LOG, log_message)
var/list/message_start
var/list/message_body = list(format_message(message))
if(!speaker)
message_start = list("<i><span class='game say'>[name]</i>: ", SPAN_FLOCKSAY_GRADIENT("System"))
else if(!ismob(speaker))
message_start = list("<i><span class='game say'>[name]</i>: ", SPAN_FLOCKSAY_GRADIENT("[speaker.name]"))
else if(isflockcontroller(speaker))
var/mob/speaking_mob = speaker
message_start = list("<i><font size=4><span class='game say'>[name]</i>: ", SPAN_FLOCKSAY_GRADIENT("[isflockmind(speaker) ? "" : "Flocktrace "][speaking_mob.real_name]"))
message_body += "</font>"
else if(isflockworker(speaker))
var/mob/speaking_mob = speaker
message_start = list("<i><span class='game say'>[name]</i>: ", SPAN_FLOCKSAY_GRADIENT("Drone [speaking_mob.real_name]"))
for(var/mob/M in GLOB.dead_mob_list)
if(!isnewplayer(M) && !isbrain(M) && speaker)
var/list/message_start_dead = list("<i><span class='game say'>[name], [SPAN_NAME("[speaker.name] ([ghost_follow_link(speaker, ghost=M)])")]")
var/list/dead_message = message_start_dead + message_body
M.show_message(dead_message.Join(" "), 2)
if(speaker && !isflockcontroller(speaker))
var/scrambled = SPAN_FLOCKSAY_GRADIENT(scramble(message))
var/living_msg = "[speaker.name] [get_spoken_verb(message)] \"[scrambled]\""
for(var/mob/M in hearers(5, get_turf(speaker)))
M.show_message(living_msg, 2)
if(M.client?.prefs.toggles2 & PREFTOGGLE_2_RUNECHAT && ismob(speaker))
var/mob/speaking_mob = speaker
M.create_chat_message(locateUID(speaking_mob.runechat_msg_location), scrambled)
for(var/mob/F in GLOB.alive_mob_list)
if(!isflockmob(F))
continue
var/list/final_message = message_start + message_body
F.show_message(final_message.Join(" "), 2)
if(F.client?.prefs.toggles2 & PREFTOGGLE_2_RUNECHAT && ismob(speaker))
var/mob/speaking_mob = speaker
F.create_chat_message(locateUID(speaking_mob.runechat_msg_location), message)
var/list/silicon_message_start = list("<i><span class='game say'>Robot Talk, [SPAN_NAME("Strange Static")]")
var/silicon_message_content = Gibberish(message, 70, replace_rate = 50)
var/silicon_message = SPAN_ROBOT(silicon_message_content)
var/list/final_message_silicon = silicon_message_start + silicon_message
if(involuntary || !prob(30))
return
for(var/mob/living/silicon/S in GLOB.alive_mob_list)
if(!S.binarycheck())
continue
S.show_message(final_message_silicon.Join(" "), 2)
if(isflockworker(speaker) && S.client?.prefs.toggles2 & PREFTOGGLE_2_RUNECHAT && ismob(speaker))
var/mob/speaking_mob = speaker
S.create_chat_message(locateUID(speaking_mob.runechat_msg_location), silicon_message_content)
@@ -0,0 +1,224 @@
/mob/living/basic/flock
name = "flockdrone"
icon = 'icons/goonstation/mob/featherzone.dmi'
icon_state = "drone"
icon_living = "drone"
icon_dead = "drone-dead"
pass_flags_self = PASSTABLE | PASSFLOCK
a_intent = INTENT_DISARM
light_color = "#26ffe6"
light_power = 0.2
light_range = 2
faction = list("flockmind")
ai_controller = /datum/ai_controller/flock
sentience_type = SENTIENCE_OTHER
minimum_survivable_temperature = 0
maximum_survivable_temperature = 1000
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)
damage_coeff = list(BRUTE = 1.5, BURN = 0.2, TOX = 0.2, CLONE = 0, STAMINA = 0, OXY = 0)
initial_traits = list(TRAIT_FLYING, TRAIT_FLOCK_THING)
butcher_results = list(/obj/item/stack/sheet/gnesis = 1)
blood_color = "#4d736d"
blood_volume = BLOOD_VOLUME_OKAY
contains_xeno_organ = TRUE
var/list/actions_to_grant = list(
/datum/action/cooldown/flock/convert,
)
var/icon_dormant = "drone-dormant"
/// Flock datum. Can be null.
var/tmp/datum/flock/flock
/// Physical resources for constructing structures or flockrunning.
var/tmp/datum/point_holder/substrate
/// Type of point holder to use
var/point_holder_type = /datum/point_holder
/// Flock ID nametag
var/tmp/obj/effect/abstract/info_tag/flock/name_tag
/// Tag for the mob's current AI task.
var/tmp/obj/effect/abstract/info_tag/flock/info/task_tag
var/bandwidth_provided = 0
/// If, while part of an active flock, a flockmob leaves the station, they become dormant.
var/tmp/dormant = FALSE
/mob/living/basic/flock/Initialize(mapload, join_flock)
. = ..()
RegisterSignal(ai_controller, COMSIG_AI_STATUS_CHANGE, PROC_REF(on_ai_status_change))
for(var/action_path in actions_to_grant)
var/datum/action/action = new action_path
action.Grant(src)
flock = join_flock || get_default_flock()
flock?.add_unit(src)
flock.add_notice(src, FLOCK_NOTICE_HEALTH)
substrate = new point_holder_type
name_tag = new()
name_tag.set_parent(src)
task_tag = new()
task_tag.set_parent(src)
add_language("Symphonic")
set_default_language(GLOB.all_languages["Symphonic"])
/mob/living/basic/flock/Destroy()
flock?.free_unit(src)
flock = null
QDEL_NULL(name_tag)
QDEL_NULL(task_tag)
return ..()
/mob/living/basic/flock/gib()
if(!death(TRUE) && stat != DEAD)
return FALSE
playsound(src.loc, 'sound/goonstation/effects/robogib.ogg', 50, 1)
flockgibs(loc)
GLOB.alive_mob_list -= src
GLOB.dead_mob_list -= src
qdel(src)
return TRUE
/mob/living/basic/flock/Process_Spacemove(movement_dir = 0, continuous_move = FALSE)
return TRUE
/mob/living/basic/flock/set_stat(new_stat)
. = ..()
if(stat != DEAD)
ADD_TRAIT(src, TRAIT_FLYING, INNATE_TRAIT)
else
REMOVE_TRAIT(src, TRAIT_FLYING, INNATE_TRAIT)
/mob/living/basic/flock/update_name(updates)
. = ..()
name_tag?.set_text(real_name)
/mob/living/basic/flock/update_icon_state()
if(stat == DEAD)
icon_state = icon_dead
else
if(dormant)
icon_state = icon_dormant
else
icon_state = icon_living
return ..()
/mob/living/basic/flock/say(message, verb, sanitize, ignore_speech_problems, ignore_atmospherics, ignore_languages, automatic, bigvoice, forced = FALSE)
. = ..()
if(!.)
return
/mob/living/basic/flock/updatehealth(cause_of_death)
. = ..()
update_health_notice()
/mob/living/basic/flock/proc/update_health_notice()
if(!flock)
return
var/datum/alternate_appearance/notice = get_alt_appearance(FLOCK_NOTICE_HEALTH)
if(!notice)
notice = flock.add_notice(src, FLOCK_NOTICE_HEALTH)
var/image/I = notice.img
I.icon_state = "hp-[round(get_damage_percent(), 10)]"
/mob/living/basic/flock/on_changed_z_level(turf/old_turf, turf/new_turf)
. = ..()
if(flock && new_turf && !flock.is_on_safe_z(new_turf))
dormantize()
/mob/living/basic/flock/death(gibbed, cause_of_death)
flock?.remove_notice(src, FLOCK_NOTICE_HEALTH)
flock?.free_unit(src)
flock?.stat_deaths++
return ..()
/mob/living/basic/flock/get_flock_id()
return real_name
/mob/living/basic/flock/proc/get_flock_data()
var/list/data = list()
data["name"] = real_name
data["health"] = get_damage_percent()
data["resources"] = substrate.has_points()
data["area"] = get_area_name(src, TRUE) || "???"
data["ref"] = ref(src)
return data
/// Become dormant. A husk. A shell.
/mob/living/basic/flock/proc/dormantize()
if(dormant)
return
dormant = TRUE
ai_controller.set_ai_status(AI_STATUS_OFF)
flock?.free_unit(src)
update_light_state()
update_appearance(UPDATE_ICON_STATE)
/mob/living/basic/flock/proc/rally(turf/location)
if(!isturf(location))
return
if(ai_controller.ai_status == AI_STATUS_OFF || ckey)
return
ai_controller.cancel_actions()
ai_controller.queue_behavior(/datum/ai_behavior/flock/rally, location)
/// Helper for keeping consistency across tesk name sets.
/mob/living/basic/flock/proc/set_task_desc(text)
task_tag?.set_text("Task: [text || "idling"]")
/// Turn the light on or off, based on if the mob is doing shit or not.
/mob/living/basic/flock/proc/update_light_state()
if(stat == DEAD || dormant)
set_light(0, 0, light_color)
return
if(ai_controller.ai_status == AI_ON || ckey)
set_light(2, 0.2, light_color)
return
set_light(0, 0, light_color)
/mob/living/basic/flock/proc/on_ai_status_change(datum/ai_controller/source, ai_status)
SIGNAL_HANDLER
update_light_state()
/mob/living/basic/flock/vv_edit_var(var_name, var_value)
switch(var_name)
if(NAMEOF(src, bandwidth_provided))
flock?.bandwidth.adjust_points(-bandwidth_provided)
..()
flock?.bandwidth.adjust_points(bandwidth_provided)
return TRUE
/mob/living/basic/flock/CanPass(atom/movable/mover, border_dir)
if(istype(mover, /obj/projectile/energy/flock_bolt))
return TRUE
return ..()
@@ -0,0 +1,47 @@
/mob/living/basic/flock/bit
name = "flockbit"
icon_state = "flockbit"
icon_dormant = "bit-dormant"
desc = "The simplest partition of the flockmind, working autonomously without heed for itself as it dutifully serves the flockmind."
move_force = MOVE_FORCE_VERY_WEAK
maxHealth = 10
health = 10
density = FALSE
pass_flags = PASSTABLE
surgery_container = /datum/xenobiology_surgery_container/flockbit
// Flockbits don't get specific AI behaviors that would make this broken.
point_holder_type = /datum/point_holder/infinite
/mob/living/basic/flock/bit/Initialize(mapload)
. = ..()
flock?.stat_bits_made++
AddComponent(/datum/component/flock_protection)
real_name = flock_realname(FLOCK_TYPE_BIT)
name = flock_name(FLOCK_TYPE_BIT)
/mob/living/basic/flock/bit/examine(mob/user)
if(!isflockmob(user))
return ..()
. = list(
SPAN_FLOCKSAY("<b>###=- Ident confirmed, data packet received.</b>"),
SPAN_FLOCKSAY("<b>ID:</b> [real_name]"),
SPAN_FLOCKSAY("<b>Flock:</b> [flock?.name || "N/A"]"),
SPAN_FLOCKSAY("<b>System Integrity: [round(health / maxHealth, 0.1) * 100]</b>"),
SPAN_FLOCKSAY("<b>Cognition:</b> [HAS_TRAIT(src, TRAIT_AI_PAUSED) ? "AWARE" : "PREDEFINED"]"),
SPAN_FLOCKSAY("<b>###=-</b>"),
)
/mob/living/basic/flock/bit/proc/i_just_split(turf/avoid)
ai_controller.pause_ai(0.3 SECONDS)
addtimer(CALLBACK(src, PROC_REF(step_away_from), avoid), 0.2 SECONDS)
/mob/living/basic/flock/bit/proc/step_away_from(turf/avoid)
if(avoid == get_turf(src))
step(src, pick(GLOB.alldirs))
return
var/list/turfs = RANGE_TURFS(1, src) - get_turf(src)
step(src, get_dir(src, pick(turfs)))
@@ -0,0 +1,470 @@
/mob/living/basic/flock/drone
desc = "The physical partitions of the flockmind, forever enacting its will until its job is complete."
hud_type = /datum/hud/flockdrone
ai_controller = /datum/ai_controller/flock/drone
move_force = MOVE_FORCE_WEAK
actions_to_grant = list(
/datum/action/cooldown/flock/release_control,
/datum/action/cooldown/flock/nest,
/datum/action/cooldown/flock/convert,
/datum/action/cooldown/flock/deconstruct,
/datum/action/cooldown/flock/flock_heal,
/datum/action/cooldown/flock/cage_mob,
/datum/action/cooldown/flock/deposit,
)
surgery_container = /datum/xenobiology_surgery_container/flockdrone
bandwidth_provided = FLOCK_COMPUTE_COST_DRONE
initial_traits = list(TRAIT_FLYING, TRAIT_FLOCK_THING, TRAIT_IMPORTANT_SPEAKER)
/// A mob possessing this mob.
var/tmp/mob/camera/flock/controlled_by
var/list/parts = list()
/// Active flockdrone part.
var/datum/flockdrone_part/active_part
/mob/living/basic/flock/drone/Initialize(mapload, join_flock)
. = ..()
create_parts()
var/datum/flockdrone_part/part = parts[1]
set_active_part(part)
flock?.stat_drones_made++
AddComponent(/datum/component/flock_protection, FALSE, TRUE, FALSE, FALSE)
real_name = flock_realname(FLOCK_TYPE_DRONE)
name = flock_name(FLOCK_TYPE_DRONE)
if(stat == CONSCIOUS)
flock_talk(src, pick(GLOB.flockdrone_created_phrases), flock, TRUE)
/mob/living/basic/flock/drone/Destroy()
release_control()
QDEL_NULL(substrate)
QDEL_LIST_CONTENTS(parts)
active_part = null // whatever was here was qdeleted by qdel_list(parts)
return ..()
/mob/living/basic/flock/drone/examine(mob/user)
if(!isflockmob(user))
return ..()
var/cognition = "TORPID"
if(stat == DEAD)
cognition = "DEAD"
else if(controlled_by || ckey)
cognition = "SAPIENT"
else if(dormant)
cognition = "ABSENT"
else if(HAS_TRAIT(src, TRAIT_AI_PAUSED))
cognition = "HIBERNATING"
. = list(
SPAN_FLOCKSAY("<b>###=- Ident confirmed, data packet received.</b>"),
controlled_by ? SPAN_FLOCKSAY("<b>ID:</b> [controlled_by.real_name] (controlling [real_name])") : SPAN_FLOCKSAY("<b>ID:</b> [real_name]"),
SPAN_FLOCKSAY("<b>Flock:</b> [flock?.name || "N/A"]"),
SPAN_FLOCKSAY("<b>Substrate: [substrate.has_points()]</b>"),
SPAN_FLOCKSAY("<b>System Integrity: [round(health / maxHealth, 0.1) * 100]</b>"),
SPAN_FLOCKSAY("<b>Cognition:</b> [cognition]"),
)
if(cognition == "TORPID" && length(ai_controller?.current_behaviors))
var/datum/ai_behavior/flock/flock_behavior = locate() in ai_controller.current_behaviors
if(istype(flock_behavior))
. += SPAN_FLOCKSAY("<b>Task: [flock_behavior.name]")
. += SPAN_FLOCKSAY("<b>###=-</b>")
/mob/living/basic/flock/drone/death(gibbed)
stop_flockphase(TRUE)
release_control()
say(pick(GLOB.flockdrone_death_phrases))
if(flock)
flock_talk(null, "Connection to drone [real_name] lost.", flock, involuntary = TRUE)
var/datum/flockdrone_part/absorber/absorber = locate() in parts
absorber.try_drop_item()
return ..()
/mob/living/basic/flock/drone/Life(seconds_per_tick, times_fired)
. = ..()
if(HAS_TRAIT(src, TRAIT_FLOCKPHASE))
if(avoid_stop_flockphase())
flockphase_tax()
else
stop_flockphase()
/mob/living/basic/flock/drone/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change)
. = ..()
// Stop existing flockphasing if we can't flockphase, also update the oldloc if it was a flock floor.
if(HAS_TRAIT(src, TRAIT_FLOCKPHASE))
if(istype(old_loc, /turf/simulated/floor/flock))
var/turf/simulated/floor/flock/flockfloor = old_loc
LAZYREMOVE(flockfloor.flockrunning_mobs, src)
flockfloor.update_power()
// Mob can't flockphase.
if(!isflockturf(loc) || !can_flockphase())
stop_flockphase(TRUE)
return
// Being in a wall forces flockphase if able.
if(istype(loc, /turf/simulated/wall/flock))
if(flockphase_tax())
start_flockphase()
return
return
// Either the client wants to flockphase, or if uncliented, already flockphasing.
var/wants_to_flockphase = HAS_TRAIT(src, TRAIT_FLOCKPHASE)
if(!wants_to_flockphase)
stop_flockphase()
return
if(istype(loc, /turf/simulated/floor/flock))
var/turf/simulated/floor/flock/flockfloor = loc
// Also incapable of flockphasing.
if(flockfloor.broken)
stop_flockphase(TRUE)
return
if(flockphase_tax())
start_flockphase()
LAZYADD(flockfloor.flockrunning_mobs, src)
flockfloor.update_power()
return
return
/mob/living/basic/flock/drone/get_status_tab_items()
var/list/status_tab_data = ..()
. = status_tab_data
status_tab_data[++status_tab_data.len] = list("Substrate: ", "[substrate.has_points()]")
status_tab_data[++status_tab_data.len] = list("", "")
switch(flock.flock_game_status)
if(NONE)
var/turf_status = min(100, floor((length(flock.claimed_floors) + length(flock.claimed_walls)) / FLOCK_TURFS_FOR_RELAY * 100))
var/bandwith_status = min(100, floor(flock.available_bandwidth() / FLOCK_COMPUTE_COST_RELAY * 100))
status_tab_data[++status_tab_data.len] = list("Bandwidth Progress: ", "[bandwith_status]%")
status_tab_data[++status_tab_data.len] = list("Area Progress: ", "[turf_status]%")
status_tab_data[++status_tab_data.len] = list("Total Relay Progress: ", "[(bandwith_status + turf_status) / 2]%")
if(FLOCK_ENDGAME_RELAY_BUILT)
status_tab_data[++status_tab_data.len] = list("Time until Broadcast: ", "[(flock.built_relay.started_time + flock.built_relay.win_time - world.time) / 10] second\s")
if(FLOCK_ENDGAME_RELAY_ACTIVATING, FLOCK_ENDGAME_VICTORY)
status_tab_data[++status_tab_data.len] = list("Time until Broadcast: ", "!!! TRANSMITTING !!!")
/mob/living/basic/flock/drone/MouseDrop_T(mob/living/M, mob/living/user)
. = ..()
if(M != user || !istype(user, /mob/camera/flock))
return
var/mob/camera/flock/ghost_bird = user
if(istype(user) && ghost_bird.flock == flock)
take_control(user)
/mob/living/basic/flock/drone/AltClick(mob/user, modifiers)
. = ..()
if(stat == DEAD)
return
var/mob/camera/flock/ghost_bird = user
if(!istype(ghost_bird))
return
var/list/choices = list()
var/image/I = image('icons/hud/radial.dmi', "radial_use")
choices["order"] = I
I = image('icons/hud/radial.dmi', "radial_control")
choices["control"] = I
var/result = show_radial_menu(user, user, choices, "[ref(usr)]_flock_click", 40)
switch(result)
if("control")
take_control(ghost_bird)
if("order")
var/datum/action/cooldown/flock/control_drone/order_action = locate() in ghost_bird.actions
if(order_action)
if(order_action.selected_bird)
order_action.unset_click_ability(usr)
else
order_action.set_click_ability(usr)
order_action.Trigger(target = src)
/mob/living/basic/flock/drone/resolve_unarmed_attack(atom/attack_target, list/modifiers)
if((a_intent == INTENT_GRAB || a_intent == INTENT_HARM) && active_part?.alt_attack)
active_part?.right_click_on(attack_target, TRUE)
else
active_part?.left_click_on(attack_target, TRUE)
/mob/living/basic/flock/drone/RangedAttack(atom/A, modifiers)
. = ..()
if(.)
return
if((a_intent == INTENT_GRAB || a_intent == INTENT_HARM) && active_part?.alt_attack)
active_part?.right_click_on(A, FALSE)
else
active_part?.left_click_on(A, FALSE)
/mob/living/basic/flock/update_health_hud()
var/severity = 0
var/healthpercent = ceil((health/maxHealth) * 100)
if(healths) // to really put you in the boots of a basic mob
var/atom/movable/screen/healths/flockdrone_health/HD = healths
switch(healthpercent)
if(100 to INFINITY)
severity = 0
if(81 to 99)
severity = 1
if(65 to 80)
severity = 2
if(49 to 64)
severity = 3
if(33 to 48)
severity = 4
if(17 to 32)
severity = 5
if(1 to 16)
severity = 6
else
severity = 7
HD.icon_state = "health[severity]"
/mob/living/basic/flock/drone/harvest(mob/living/user)
var/list/loot = list(
/obj/item/stack/sheet/gnesis = 1,
/obj/item/shard/gnesis_glass = 1,
)
for(var/i in 3 to 6)
var/path = pickweight(loot)
new path(drop_location())
playsound(src, 'sound/effects/pylon_shatter.ogg', 30, TRUE, SILENCED_SOUND_EXTRARANGE)
// Spawn flock organs here
qdel(src)
/mob/living/basic/flock/drone/get_flock_data()
var/list/data = ..()
var/current_behavior_name
if(controlled_by)
data["task"] = "controlled"
data["controller_ref"] = ref(controlled_by)
else if((ai_controller.ai_status == AI_ON) && length(ai_controller.current_behaviors))
var/datum/ai_behavior/flock/current_task = ai_controller.current_behaviors[1]
if(istype(current_task))
current_behavior_name = current_task.name
data["task"] = current_behavior_name || "hibernating"
return data
/mob/living/basic/flock/drone/on_ai_status_change(datum/ai_controller/source, ai_status)
. = ..()
if(ai_status == AI_STATUS_OFF && controlled_by)
task_tag.set_text("Controlled By: [controlled_by.real_name]")
/mob/living/basic/flock/drone/dormantize()
if(!flock)
return ..()
if(controlled_by)
release_control(FALSE)
flock_talk(null, "Connection to drone [real_name] lost.", flock, involuntary = TRUE)
spawn(-1)
say("error: out of signal range, disconnecting")
return ..()
/// Sets the active flockdrone part.
/mob/living/basic/flock/drone/proc/set_active_part(datum/flockdrone_part/new_part)
var/datum/flockdrone_part/old_part = active_part
active_part = new_part
active_part.screen_obj?.update_appearance()
old_part?.screen_obj?.update_appearance()
/// Create all of the part datums for this mob.
/mob/living/basic/flock/drone/proc/create_parts()
parts += new /datum/flockdrone_part/converter(src)
parts += new /datum/flockdrone_part/incapacitator(src)
parts += new /datum/flockdrone_part/absorber(src)
/mob/living/basic/flock/drone/proc/start_flockphase()
if(HAS_TRAIT(src, TRAIT_FLOCKPHASE))
return FALSE
playsound(src, 'sound/goonstation/flockmind/flockdrone_floorrun.ogg', 30, TRUE, extrarange = SHORT_RANGE_SOUND_EXTRARANGE)
ADD_TRAIT(src, TRAIT_FLOCKPHASE, INNATE_TRAIT)
pass_flags_self |= LETPASSTHROW | PASSFLOCK
mob_size = 0
set_density(FALSE)
for(var/obj/item/grab/G in contents)
qdel(G)
move_speed -= 0.4
var/list/color_matrix = list(1,0,0, 0,1,0, 0,0,1, 0.15,0.77,0.66)
var/matrix/shrink = matrix().Scale(0)
animate(src, color = color_matrix, transform = shrink, time = 0.5 SECONDS, easing = SINE_EASING)
return TRUE
/mob/living/basic/flock/drone/proc/stop_flockphase(force)
if(!HAS_TRAIT(src, TRAIT_FLOCKPHASE))
return FALSE
if(!force && avoid_stop_flockphase())
return FALSE
playsound(src, 'sound/goonstation/flockmind/flockdrone_floorrun.ogg', 30, TRUE, extrarange = SHORT_RANGE_SOUND_EXTRARANGE)
REMOVE_TRAIT(src, TRAIT_FLOCKPHASE, INNATE_TRAIT)
pass_flags_self &= ~(LETPASSTHROW | PASSFLOCK)
mob_size = initial(mob_size)
set_density(TRUE)
move_speed += 0.4
animate(src, color = null, transform = null, time = 0.5 SECONDS, easing = SINE_EASING)
if(!isturf(loc))
return
if(istype(loc, /turf/simulated/floor/flock))
var/turf/simulated/floor/flock/flockfloor = loc
LAZYREMOVE(flockfloor.flockrunning_mobs, src)
flockfloor.update_power()
var/turf/turfloc = loc
if(turfloc.can_flock_occupy(src))
return
for(var/turf/T in get_adjacent_open_turfs(src))
if(!T.can_flock_occupy(src))
forceMove(T)
break
/// Called in stop_flockphase() to attempt stopping flockphase due to being in a wall or something.
/mob/living/basic/flock/drone/proc/avoid_stop_flockphase()
if(!can_flockphase())
return FALSE
var/turf/T = loc
if(T.density)
return TRUE
/// Returns TRUE if the drone can flockphase.
/mob/living/basic/flock/drone/proc/can_flockphase()
if(stat != CONSCIOUS)
return FALSE
if(length(grabbed_by))
return FALSE
if(!substrate.has_points())
return FALSE
return TRUE
/// Deducts the substrate tax for flockphasing, ending flockphase if the drone ran out of points.
/mob/living/basic/flock/drone/proc/flockphase_tax()
substrate.remove_points(1)
if(!substrate.has_points())
stop_flockphase(TRUE)
return FALSE
return TRUE
/mob/living/basic/flock/drone/proc/take_control(mob/camera/flock/master_bird)
if(HAS_TRAIT_FROM(src, TRAIT_AI_DISABLE_PLANNING, FLOCK_CONTROLLED_BY_OVERMIND_SOURCE))
to_chat(master_bird, SPAN_ALERT("This drone is recieving a sentient-level instruction."))
return FALSE
if(controlled_by)
to_chat(master_bird, SPAN_ALERT("This drone is already under another partition's command."))
return FALSE
stop_flockphase()
controlled_by = master_bird
controlled_by.controlling_bird = src
if(controlled_by.mind)
key = controlled_by.key
mind = controlled_by.mind
if(isflocktrace(controlled_by))
flock.add_notice(src, FLOCK_NOTICE_FLOCKTRACE_CONTROL)
else
flock.add_notice(src, FLOCK_NOTICE_FLOCKMIND_CONTROL)
to_chat(src, SPAN_FLOCKSAY("<b>\[SYSTEM: Control of drone [real_name] established.\]</b>"))
return TRUE
/mob/living/basic/flock/drone/proc/release_control(go_dormant = FALSE)
if(isnull(controlled_by))
return
task_tag.set_text("")
var/dest_was_safe = TRUE
var/turf/destination = get_turf(src)
if(!flock.is_on_safe_z(destination))
dest_was_safe = FALSE
destination = get_turf(pick(flock.drones)) || get_safe_random_station_turf()
var/mob/camera/flock/master_bird = controlled_by
controlled_by = null
if(flock)
flock.remove_notice(src, FLOCK_NOTICE_FLOCKMIND_CONTROL)
flock.remove_notice(src, FLOCK_NOTICE_FLOCKTRACE_CONTROL)
if(isnull(master_bird) && ckey)
if(flock)
master_bird = new /mob/camera/flock/trace(destination, flock)
else
ghostize(FALSE)
if(!master_bird)
return
master_bird.forceMove(destination)
if(mind)
master_bird.key = key
master_bird.mind = mind
flock_talk(null, "Control of [real_name] surrendered.", flock, involuntary = TRUE)
if(!dest_was_safe)
to_chat(master_bird, SPAN_WARNING("You feel your consciousness weaking as you are ripped further from your rift, and you retreat back to safety."))
if(!flock && go_dormant)
dormantize()
/mob/living/basic/flock/drone/proc/split_into_bits()
ai_controller.pause_ai(3 SECONDS)
say("\[System notification: drone diffracting.\]")
flock?.free_unit(src)
var/turf/T = get_turf(src)
for(var/i in 1 to 3)
var/mob/living/basic/flock/bit/bitty_bird = new(T, flock)
bitty_bird.i_just_split(T)
var/list/new_color = list(1,0,0, 0,1,0, 0,0,1, 0.15,0.77,0.66)
color = null
animate(src, color=new_color, alpha = 0, time = 0.5 SECONDS)
addtimer(CALLBACK(src, PROC_REF(finish_splitting)), 0.5 SECONDS)
/mob/living/basic/flock/drone/proc/finish_splitting()
qdel(src)
@@ -0,0 +1,95 @@
/datum/flockdrone_part/absorber
var/obj/item/held_item
/// Damage rate
var/absorption_rate = 4
/// Per point of integrity, generate this much substrate.
var/integrity_substrate_ratio = 2
/datum/flockdrone_part/absorber/Destroy(force, ...)
QDEL_NULL(held_item)
return ..()
/datum/flockdrone_part/absorber/left_click_on(atom/target, in_reach)
var/obj/item/I = target
if(!in_reach || !istype(I) || !isturf(I.loc))
return
return try_pickup_item(I)
/datum/flockdrone_part/absorber/process(seconds_per_tick)
var/added = 0
var/obj/item/eating = held_item
if(istype(eating, /obj/item/flock_cube))
var/obj/item/flock_cube/cube = eating
added = cube.substrate
drone.substrate.add_points(added)
qdel(cube)
to_chat(drone, SPAN_NOTICE("We decompile the resource cache, adding <b>[added]</b> substrate to our reserves."))
else
added = eating.take_damage(absorption_rate * seconds_per_tick, BRUTE, ACID, sound_effect = FALSE, armor_penetration_percentage = 100)
drone.substrate.add_points(added * integrity_substrate_ratio)
if(!held_item) // if take_damage qdeletes it, it becomes null due to signal stuff.
to_chat(drone, SPAN_NOTICE("We finish converting [eating] into substrate."))
return
playsound(drone, "sparks", 30, TRUE, extrarange = SILENCED_SOUND_EXTRARANGE)
/datum/flockdrone_part/absorber/proc/try_pickup_item(obj/item/I)
if(held_item)
return FALSE
if(I.flags & ABSTRACT)
return FALSE
drone.face_atom(I)
I.forceMove(drone)
if(QDELETED(I))
return FALSE
held_item = I
RegisterSignal(I, list(COMSIG_MOVABLE_MOVED, COMSIG_PARENT_QDELETING), PROC_REF(held_item_moved))
START_PROCESSING(SSprocessing, src)
var/matrix/first_matrix = matrix()
first_matrix.Turn(-45)
first_matrix.Scale(1.2, 0.6)
var/matrix/second_matrix = matrix()
first_matrix.Turn(45)
first_matrix.Scale(0.6, 1.2)
animate(held_item, loop= -1, color="#00ffd7", transform= first_matrix, time = 2 SECONDS, tag = "flockdrone_eat")
animate(color = "#ffffff", transform = second_matrix, time = 2 SECONDS)
screen_obj?.vis_contents += held_item
return TRUE
/datum/flockdrone_part/absorber/proc/try_drop_item(move = TRUE)
if(!held_item)
return FALSE
UnregisterSignal(held_item, list(COMSIG_MOVABLE_MOVED, COMSIG_PARENT_QDELETING))
var/obj/item/old_item = held_item
animate(held_item, tag = "flockdrone_eat")
held_item = null
STOP_PROCESSING(SSprocessing, src)
screen_obj?.vis_contents -= old_item
if(!move)
return TRUE
var/turf/drop_loc = drone.drop_location()
if(drop_loc)
old_item.forceMove(drop_loc)
else
qdel(old_item)
return TRUE
/datum/flockdrone_part/absorber/proc/held_item_moved(datum/source)
SIGNAL_HANDLER
try_drop_item(FALSE)
@@ -0,0 +1,36 @@
/datum/flockdrone_part/converter
alt_attack = TRUE
/datum/flockdrone_part/converter/left_click_on(atom/target, in_reach)
if(!in_reach)
return
if(isliving(target))
return try_cage(target)
if(istype(target, /obj/structure/flock/tealprint))
var/datum/action/cooldown/flock/deposit/deposit_action = locate() in drone.actions
return deposit_action.Trigger(target = target)
if(istype(target, /obj/structure/closet))
var/obj/structure/closet/C = target
C.toggle()
var/turf/T = get_turf(target)
var/datum/action/cooldown/flock/convert/convert_action = locate() in drone.actions
return convert_action.Trigger(target = T)
/datum/flockdrone_part/converter/right_click_on(atom/target, in_reach)
if(!in_reach)
return
var/datum/action/cooldown/flock/deconstruct/decon_action = locate() in drone.actions
return decon_action.Trigger(target = target)
/datum/flockdrone_part/converter/proc/try_cage(mob/living/victim)
if(isflockmob(victim))
to_chat(drone, SPAN_WARNING("ERROR: Unable to imprison substrate construct."))
return FALSE
var/datum/action/cooldown/flock/cage_mob/cage_action = locate() in drone.actions
return cage_action.Trigger(target = victim)
@@ -0,0 +1,26 @@
/datum/flockdrone_part
var/mob/living/basic/flock/drone/drone
/// Reference to the associated screen object, if it exists (grr mobs that havent had a player connected)
var/atom/movable/screen/flockdrone_part/screen_obj
/// If the part has an alt-attack
var/alt_attack = FALSE
/datum/flockdrone_part/New(drone)
src.drone = drone
/datum/flockdrone_part/Destroy(force, ...)
screen_obj?.part_ref = null
screen_obj = null
drone = null
return ..()
/datum/flockdrone_part/proc/is_active()
return drone.active_part == src
/// Called when a drone with this part active left clicks on an atom. in_reach is TRUE if the target atom is reachable.
/datum/flockdrone_part/proc/left_click_on(atom/target, in_reach)
return
/// Called when a drone with this part active right clicks on an atom. in_reach is TRUE if the target atom is reachable.
/datum/flockdrone_part/proc/right_click_on(atom/target, in_reach)
return
@@ -0,0 +1,54 @@
/datum/flockdrone_part/incapacitator
/// Maximum shots that can be stored
var/max_shots = 5
/// How many shots we have left
var/shot_count = 5
/// Time between shots cooldown
COOLDOWN_DECLARE(shoot_cd)
/// Time between shots
var/time_between_shots = 1.2 SECONDS
/// Time to gain 1 charge.
var/recharge_time = 4 SECONDS
var/recharge_timer_id
/datum/flockdrone_part/incapacitator/left_click_on(atom/target, in_reach)
if(!COOLDOWN_FINISHED(src, shoot_cd))
return FALSE
if(!shot_count)
return FALSE
if(istype(target, /atom/movable/screen))
return FALSE
shot_count -= 1
screen_obj?.update_appearance()
var/obj/projectile/energy/flock_bolt/bolt = new(get_turf(drone))
bolt.preparePixelProjectile(target, drone)
bolt.firer = drone
bolt.firer_source_atom = src
bolt.def_zone = drone.zone_selected
bolt.fire()
playsound(drone, 'sound/weapons/resonator_fire.ogg', 50, TRUE)
if(!recharge_timer_id)
recharge_timer_id = addtimer(CALLBACK(src, PROC_REF(recharge)), recharge_time)
COOLDOWN_START(src, shoot_cd, time_between_shots)
return TRUE
/// Increments shot count by 1 and starts the recharge timer if necessary.
/datum/flockdrone_part/incapacitator/proc/recharge()
shot_count = min(shot_count + 1, max_shots)
screen_obj?.update_appearance()
if(shot_count == max_shots)
recharge_timer_id = null
else
recharge_timer_id = addtimer(CALLBACK(src, PROC_REF(recharge)), recharge_time)
@@ -0,0 +1,7 @@
/datum/objective/flock_relay
name = "Broadcast"
explanation_text = "Broadcast the Signal."
/datum/objective/flock_relay/check_completion()
var/mob/camera/flock/ghostbird = owner.current
return ghostbird.flock.flock_game_status == FLOCK_ENDGAME_VICTORY
@@ -0,0 +1,13 @@
/mob/camera/flock/say(message)
if(!message)
return
flock_talk(src, message, flock)
/proc/flock_talk(atom/speaker, message, datum/flock/flock, involuntary = FALSE)
message = trim(copytext_char(sanitize(message), 1, MAX_MESSAGE_LEN))
if(!message)
return
var/datum/language/flock/symphonic = GLOB.all_languages["Symphonic"]
symphonic.broadcast(speaker, message, involuntary = involuntary)
@@ -0,0 +1,228 @@
/obj/structure/flock
name = "CALL A CODER AAAAAAAAAA"
desc = ABSTRACT_TYPE_DESC
icon = 'icons/goonstation/mob/featherzone.dmi'
anchored = TRUE
density = TRUE
max_integrity = 50
armor = list(MELEE = -20, BULLET = -20, LASER = 80, ENERGY = 80, BOMB = 0, RAD = 100, FIRE = 80, ACID = 100)
var/datum/flock/flock
/// Info tag for the flock name of the structure.
var/obj/effect/abstract/info_tag/flock/name_tag
/// Info tag for the actual information of the structure.
var/obj/effect/abstract/info_tag/flock/info/info_tag
var/flock_id
/// Shown in the flockmind UI
var/flock_desc
/// How much juice it takes to construct
var/resource_cost = 0
/// How long it takes to build
var/build_time = 0
/// Is the tealprint cancellable
var/cancellable = TRUE
/// Is the structure finished?
var/fully_built = FALSE
/// If TRUE, flockdrones cannot deconstruct this.
var/no_flock_decon = FALSE
/// world.time this was created at
var/spawn_time
/// How much bandwidth this structure provides to the Flock.
var/bandwidth_provided = 0
/// Whether or not the turret is active. The state of the Flock can change this.
var/active = FALSE
/// The bandwidth cost while active
var/active_bandwidth_cost = 0
/// Allows flockdrones to pass through.
var/allow_flockpass = TRUE
COOLDOWN_DECLARE(scream_cd)
/obj/structure/flock/Initialize(mapload, datum/flock/join_flock)
. = ..()
spawn_time = world.time
join_flock ||= get_default_flock()
if(join_flock)
join_flock.add_structure(src)
AddComponent(/datum/component/flock_object, join_flock)
var/static/list/loc_connections = list(
COMSIG_ATOM_ENTERED = PROC_REF(on_crossed),
)
AddElement(/datum/element/connect_loc, loc_connections)
if(build_time)
START_PROCESSING(SSobj, src)
else
finish_building()
ADD_TRAIT(src, TRAIT_FLOCK_EXAMINE, INNATE_TRAIT)
if(no_flock_decon)
ADD_TRAIT(src, TRAIT_FLOCK_NODECON, INNATE_TRAIT)
name_tag = new()
name_tag.set_parent(src)
name_tag.set_text(flock_id)
info_tag = new()
info_tag.set_parent(src)
return INITIALIZE_HINT_LATELOAD
/obj/structure/flock/LateInitialize()
. = ..()
update_info_tag()
/obj/structure/flock/Destroy()
STOP_PROCESSING(SSobj, src)
flock?.free_structure(src)
QDEL_NULL(name_tag)
QDEL_NULL(info_tag)
return ..()
/obj/structure/flock/get_flock_id()
return flock_id
/obj/structure/flock/proc/get_flock_data()
. = list()
.["ref"] = ref(src)
.["name"] = flock_id
.["health"] = get_integrity_percentage()
.["compute"] = active ? -active_bandwidth_cost : bandwidth_provided
.["desc"] = flock_desc
.["area"] = get_area_name(src, TRUE) || "???"
/obj/structure/flock/deconstruct(disassembled)
visible_message(SPAN_WARNING("[src] dissolves into nothingness."))
var/refund = round(get_integrity_percentage() * (disassembled ? 1 : 0.5) * resource_cost, 1)
if(refund)
var/obj/item/flock_cube/cube = new(drop_location())
cube.substrate = refund
return ..()
/obj/structure/flock/CanPass(atom/movable/mover, border_dir)
. = ..()
if(allow_flockpass)
return isflockdrone(mover)
/obj/structure/flock/CanPathfindPass(to_dir, datum/can_pass_info/pass_info)
var/atom/M = locateUID(pass_info.caller_uid)
if(!M)
return ..()
if(allow_flockpass)
return isflockdrone(M)
return ..()
/obj/structure/flock/try_flock_convert(datum/flock/flock, force)
return
/obj/structure/flock/attack_hand(mob/living/user, list/modifiers)
. = ..()
if(.)
return
if(user.a_intent == INTENT_HARM)
user.visible_message(SPAN_DANGER("<b>[user]</b> punches <b>[src]."))
user.do_attack_animation(src, ATTACK_EFFECT_PUNCH)
//playsound(src, 'sound/impact_sounds/Crystal_Hit_1.ogg', 50, TRUE, -1)
bitch_and_moan()
take_damage(1, BRUTE)
return TRUE
/obj/structure/flock/attacked_by(obj/item/attacking_item, mob/living/user)
. = ..()
if(!.)
return
bitch_and_moan()
// if(. < 5)
// playsound(src, 'sound/impact_sounds/Crystal_Hit_1.ogg', 50, TRUE)
// else
// playsound(src, 'sound/impact_sounds/Glass_Shards_Hit_1.ogg', 50, TRUE)
/obj/structure/flock/process()
update_info_tag()
if(spawn_time + build_time <= world.time)
finish_building()
return
/// Returns the number of seconds remaining for the build.
/obj/structure/flock/proc/build_time_left()
return ((spawn_time + build_time) - world.time) / 10
/obj/structure/flock/proc/set_active(new_state)
if(active == new_state)
return
active = new_state
update_appearance()
if(active)
flock.remove_bandwidth_influence(bandwidth_provided)
flock.add_bandwidth_influence(-active_bandwidth_cost)
else
flock.remove_bandwidth_influence(-active_bandwidth_cost)
flock.add_bandwidth_influence(bandwidth_provided)
return new_state
/// Called when an object finishes construction
/obj/structure/flock/proc/finish_building()
SHOULD_CALL_PARENT(TRUE)
STOP_PROCESSING(SSobj, src)
fully_built = TRUE
/obj/structure/flock/examine(mob/user)
if(!isflockmob(user))
return ..()
. = list(
SPAN_FLOCKSAY("<b>###=- Ident confirmed, data packet received.</b>"),
SPAN_FLOCKSAY("<b>ID:</b> [get_flock_id()]"),
SPAN_FLOCKSAY("<b>Flock:</b> [flock?.name || "N/A"]"),
SPAN_FLOCKSAY("<b>System Integrity:</b> [get_integrity_percentage()]%"),
)
if(!fully_built)
. += SPAN_FLOCKSAY("<b>Time Left:</b> [build_time_left()] seconds")
var/list/additional_lines = flock_structure_examine(user)
if(length(additional_lines))
. += additional_lines
. += SPAN_FLOCKSAY("<b>###=-</b>")
/obj/structure/flock/proc/flock_structure_examine(mob/user)
return
/// Stub for children to set their info in process() and initialize()
/obj/structure/flock/proc/update_info_tag()
return
/obj/structure/flock/proc/on_crossed(atom/source, atom/movable/crosser)
SIGNAL_HANDLER
if(!allow_flockpass || !isflockdrone(crosser))
return
if(!HAS_TRAIT(crosser, TRAIT_FLOCKPHASE))
animate_flockpass(crosser)
/obj/structure/flock/proc/bitch_and_moan()
if(!COOLDOWN_FINISHED(src, scream_cd))
return
COOLDOWN_START(src, scream_cd, 10 SECONDS)
flock_talk(src, "WARNING: Under attack", flock)
@@ -0,0 +1,272 @@
/obj/structure/flock/cage
name = "energy cage"
desc = "A digitized energy structure that appears to turn matter into gnesis."
icon_state = "cage"
alpha = 190
anchored = FALSE
max_integrity = 30
flock_desc = "Converts organic creatures into Flockdrones."
flock_id = "Matter reprocessor"
var/tmp/mob/living/victim
/// Current item being munched on.
var/tmp/obj/item/eating
/// With this much gnesis, create an egg.
var/egg_gnesis_cost = 100
/// Rate of damage to items being consumed by the cage
var/absorption_rate = 4
/// Per point of damage, generate this much substrate.
var/integrity_substrate_ratio = 2
/// Timer until you can ghost from your body without penalty
var/ghost_timer
COOLDOWN_DECLARE(flock_message_cd)
COOLDOWN_DECLARE(relaymove_cd)
/obj/structure/flock/cage/Initialize(mapload, datum/flock/join_flock)
. = ..()
create_reagents(200)
AddComponent(/datum/component/flock_protection)
/obj/structure/flock/cage/Destroy()
QDEL_NULL(victim)
QDEL_NULL(eating)
return ..()
/obj/structure/flock/cage/deconstruct(disassembled)
if(disassembled)
spend_gnesis(TRUE)
else
reagents.reaction(get_turf(src), REAGENT_TOUCH)
if(victim)
visible_message(SPAN_WARNING("[victim] breaks free from [src]."))
set_victim(null)
var/drop_loc = drop_location()
for(var/atom/movable/AM as anything in contents)
AM.forceMove(drop_loc)
. = ..()
/obj/structure/flock/cage/process(seconds_per_tick)
spend_gnesis()
if(victim && flock)
flock.update_enemy(victim)
if(QDELETED(eating))
eating = null
if(!eating)
var/obj/item/edibles = list()
for(var/obj/item/edible in src)
edibles += edible
if(length(edibles))
set_eating_target(pick(edibles))
playsound(src, 'sound/goonstation/weapons/nano-blade-1.ogg', 50, TRUE)
if(victim?.stat == CONSCIOUS)
to_chat(victim, SPAN_WARNING("[eating] begins to melt away."))
else
chew_on_mob(seconds_per_tick)
else
var/added = eating.take_damage(absorption_rate * seconds_per_tick * 25, BRUTE, armor_penetration_percentage = 100)
reagents.add_reagent(/datum/reagent/gnesis, integrity_substrate_ratio * added)
if(victim && COOLDOWN_FINISHED(src, flock_message_cd))
COOLDOWN_START(src, flock_message_cd, rand(10, 25) SECONDS)
to_chat(victim, SPAN_FLOCKSAY("<i>[pick(strings("flock.json", "conversion"))]</i>"))
if(!victim) // Victim gibbed
deconstruct(TRUE)
/obj/structure/flock/cage/container_resist(mob/living/user)
to_chat(victim, SPAN_WARNING("You resist against the confines of the cage!"))
if(!do_after_once(user, 3 SECONDS, FALSE, src, interaction_key = "flock_cage_resist"))
return
audible_message("[src] [pick("cracks","bends","shakes","groans")].")
playsound(
src,
pick('sound/goonstation/flockmind/flockdrone_grump1.ogg', 'sound/goonstation/flockmind/flockdrone_grump2.ogg', 'sound/goonstation/flockmind/flockdrone_grump3.ogg'),
50,
TRUE
)
take_damage(7, BRUTE)
/obj/structure/flock/cage/relaymove(mob/living/user, direction)
if(!COOLDOWN_FINISHED(src, relaymove_cd))
return
COOLDOWN_START(src, relaymove_cd, 1 SECONDS)
if(!prob(80))
return
if(prob(20))
audible_message("[src] [pick("cracks", "bends", "shakes", "groans")].")
take_damage(1, BRUTE)
/obj/structure/flock/cage/flock_structure_examine(mob/user)
return list(
SPAN_FLOCKSAY("<b>Volume:</b> [reagents.get_reagent_amount(/datum/reagent/gnesis)]"),
SPAN_FLOCKSAY("<b>Needed volume:</b> [egg_gnesis_cost]<br>"),
)
/// Picks an item, organ, or bodypart, to munch on.
/obj/structure/flock/cage/proc/chew_on_mob(seconds_per_tick)
if(!ishuman(victim))
victim.adjustBruteLoss(absorption_rate * seconds_per_tick)
if(victim.stat == DEAD)
visible_message(SPAN_DANGER("[src] rips apart what remains of [victim]."))
victim.gib()
set_victim(null)
return
var/mob/living/carbon/human/human_victim = victim
var/list/items = human_victim.get_equipped_items()
if(length(items))
while(length(items) && !eating)
var/obj/item/candidate = pick_n_take(items)
if(candidate.resistance_flags & INDESTRUCTIBLE)
human_victim.drop_item_to_ground(candidate, TRUE, TRUE)
visible_message(SPAN_WARNING("[src] pulls [eating] from [human_victim] and drops it!"))
continue
human_victim.transfer_item_to(candidate, src, TRUE, TRUE)
if(!QDELETED(candidate))
set_eating_target(candidate)
if(eating)
visible_message(SPAN_WARNING("[src] pulls [eating] from [human_victim] and begins ripping it apart."))
playsound(src, 'sound/goonstation/weapons/nano-blade-1.ogg', 50, TRUE)
return
var/list/bodyparts = human_victim.bodyparts.Copy()
for(var/obj/item/organ/external/BP in bodyparts)
if(BP.body_part in list(HEAD, UPPER_TORSO, LOWER_TORSO))
bodyparts -= BP
if(length(bodyparts))
var/obj/item/organ/external/yummy_appendage = pick(bodyparts)
human_victim.pain(yummy_appendage.name, 85)
yummy_appendage.droplimb(FALSE)
set_eating_target(yummy_appendage)
eating.forceMove(src)
visible_message(SPAN_DANGER("[src] tears [eating] from [human_victim] and begins ripping it apart."))
return
var/list/skip_organs = list("eyes", "ears", "brain", "heart", "lungs")
for(var/obj/item/organ/internal/O in bodyparts)
if(O.slot in skip_organs)
bodyparts -= O
if(length(bodyparts))
var/obj/item/organ/internal/yummy_organ = pick(bodyparts)
var/organ_loc_str = yummy_organ.parent_organ
human_victim.pain(organ_loc_str, 85)
yummy_organ.remove(human_victim)
if(!QDELING(yummy_organ))
set_eating_target(yummy_organ)
eating.forceMove(src)
visible_message(SPAN_DANGER("[src] tears [eating] from [human_victim]'s [organ_loc_str] and begins ripping it apart."))
return
visible_message(SPAN_DANGER("[src] rips apart what remains of [human_victim]."))
set_victim(null)
human_victim.gib(TRUE, TRUE, TRUE)
// INTO THE CAGE
/obj/structure/flock/cage/proc/cage_mob(mob/living/L)
L.forceMove(src)
set_victim(L)
victim.visible_message(SPAN_DANGER("A [name] materializes around [victim],"))
ghost_timer = addtimer(CALLBACK(src, PROC_REF(ghost_check), L), 15 SECONDS, TIMER_UNIQUE|TIMER_STOPPABLE)
/// Spends gnesis on eggs or a cube. Only spends on eggs by default.
/obj/structure/flock/cage/proc/spend_gnesis(all = FALSE)
var/egg_spawn_count = 0
var/spend_on_cube = 0
// Get egg count and spend the gnesis.
while(reagents.has_reagent(/datum/reagent/gnesis, egg_gnesis_cost))
reagents.remove_reagent(/datum/reagent/gnesis, egg_gnesis_cost)
egg_spawn_count++
// Spawn eggs or pool it to the cube
for(var/i in 1 to egg_spawn_count)
if(length(flock?.drones) <= FLOCK_DRONE_LIMIT)
var/obj/structure/flock/egg/egg = new(drop_location(), flock)
egg.throw_at(get_edge_target_turf(egg, pick(GLOB.alldirs)), 6, 3)
else
spend_on_cube += egg_gnesis_cost
// If we're dumping it all, collect the remaining gnesis
if(all)
spend_on_cube += reagents.get_reagent_amount(/datum/reagent/gnesis)
// Cube
if(spend_on_cube && all)
reagents.remove_reagent(/datum/reagent/gnesis, spend_on_cube)
var/obj/item/flock_cube/cube = new
cube.substrate = spend_on_cube
cube.forceMove(drop_location())
/// Setter for eating, no side effects.
/obj/structure/flock/cage/proc/set_eating_target(obj/item/new_eating)
if(eating)
UnregisterSignal(eating, COMSIG_MOVABLE_MOVED)
eating = new_eating
if(!eating)
return
RegisterSignal(eating, COMSIG_MOVABLE_MOVED, PROC_REF(target_gone))
/// Setter for victim, no side effects.
/obj/structure/flock/cage/proc/set_victim(mob/living/new_victim)
if(victim)
UnregisterSignal(victim, COMSIG_MOVABLE_MOVED)
victim.clear_fullscreen("flock_convert")
victim = new_victim
if(!victim)
STOP_PROCESSING(SSobj, src)
return
START_PROCESSING(SSobj, src)
RegisterSignal(victim, COMSIG_MOVABLE_MOVED, PROC_REF(victim_gone))
victim.overlay_fullscreen("flock_convert", /atom/movable/screen/fullscreen/flock_convert)
/obj/structure/flock/cage/proc/victim_gone(datum/source)
SIGNAL_HANDLER
if(!QDELING(src))
deconstruct(FALSE)
/obj/structure/flock/cage/proc/target_gone(datum/source)
SIGNAL_HANDLER
set_eating_target(null)
/obj/structure/flock/cage/proc/ghost_check(mob/user)
victim.throw_alert("ghost_cage", /atom/movable/screen/alert/ghost/flock)
to_chat(victim, SPAN_GHOSTALERT("You may now click on the ghost prompt on your screen to leave your body. You will be alerted when you're removed from the cage."))
if(tgui_alert(victim, "You may now ghost and keep respawnability. You will be notified if you leave the cage, would you like to do so?", "Ghosting", list("Yes", "No")) != "Yes")
return
victim.ghostize(GHOST_FLAGS_DEFAULT, ghost_color = "#19b299")
/mob/living/proc/test_cage()
var/obj/structure/flock/cage/cage = new(get_turf(src))
cage.cage_mob(src)
@@ -0,0 +1,27 @@
/obj/structure/chair/comfy/flock
name = "buzzing alcove"
desc = "It's a chair made of a strange material. Surprisingly soft to the touch, and extremely out of style."
icon_state = "chair_flock"
buildstacktype = /obj/item/stack/sheet/gnesis
max_integrity = 50
armor = list(MELEE = -20, BULLET = -20, LASER = 80, ENERGY = 80, BOMB = 0, RAD = 100, FIRE = 80, ACID = 100)
/obj/structure/chair/comfy/flock/Initialize(mapload, datum/flock/join_flock)
. = ..()
AddComponent(/datum/component/flock_protection, FALSE, TRUE, FALSE, FALSE)
ADD_TRAIT(src, TRAIT_FLOCK_EXAMINE, INNATE_TRAIT)
ADD_TRAIT(src, TRAIT_FLOCK_THING, INNATE_TRAIT)
/obj/structure/chair/comfy/flock/examine(mob/user)
if(!isflockmob(user))
return ..()
. = list(
SPAN_FLOCKSAY("<b>###=- Ident confirmed, data packet received.</b>"),
SPAN_FLOCKSAY("<b>ID:</b> Resting Chamber"),
SPAN_FLOCKSAY("<b>System Integrity:</b> [get_integrity_percentage()]%"),
SPAN_FLOCKSAY("<b>###=-</b>")
)
/obj/structure/chair/comfy/flock/try_flock_convert(datum/flock/flock, force)
return
@@ -0,0 +1,68 @@
/obj/structure/closet/flock
name = "pulsing cache"
desc = "A large closet-like structure that appears to lack a handle. The doors seem to open to the touch."
icon_state = "flock"
max_integrity = 100
armor = list(MELEE = -20, BULLET = -20, LASER = 80, ENERGY = 80, BOMB = 0, RAD = 100, FIRE = 80, ACID = 100)
pass_flags = PASSFLOCK
material_drop = /obj/item/stack/sheet/gnesis
enable_door_overlay = FALSE
/obj/structure/closet/flock/Initialize(mapload, datum/flock/join_flock)
. = ..()
var/static/list/loc_connections = list(
COMSIG_ATOM_ENTERED = PROC_REF(on_crossed),
)
AddElement(/datum/element/connect_loc, loc_connections)
AddComponent(/datum/component/flock_protection, FALSE, TRUE, FALSE, FALSE)
ADD_TRAIT(src, TRAIT_FLOCK_EXAMINE, INNATE_TRAIT)
ADD_TRAIT(src, TRAIT_FLOCK_THING, INNATE_TRAIT)
/obj/structure/closet/flock/proc/on_crossed(atom/source, atom/movable/crosser)
SIGNAL_HANDLER
if(!isflockdrone(crosser) || broken)
return
if(!HAS_TRAIT(crosser, TRAIT_FLOCKPHASE))
animate_flockpass(crosser)
/obj/structure/closet/flock/CanPass(atom/movable/mover, border_dir)
. = ..()
if(.)
return .
if(!isflockdrone(mover))
return .
var/mob/living/basic/flock/drone/bird = mover
if(HAS_TRAIT(bird, TRAIT_FLOCKPHASE))
return TRUE
if(bird.can_flockphase())
return TRUE
/obj/structure/closet/flock/CanPathfindPass(to_dir, datum/can_pass_info/pass_info)
var/atom/M = locateUID(pass_info.caller_uid)
if(!M)
return ..()
if(!isflockdrone(M))
return ..()
var/mob/living/basic/flock/drone/bird = M
if(!bird.can_flockphase())
return ..()
return TRUE
/obj/structure/closet/flock/examine(mob/user)
if(!isflockmob(user))
return ..()
. = list(
SPAN_FLOCKSAY("<b>###=- Ident confirmed, data packet received.</b>"),
SPAN_FLOCKSAY("<b>ID:</b> Storage Capsule"),
SPAN_FLOCKSAY("<b>System Integrity:</b> [get_integrity_percentage()]%"),
SPAN_FLOCKSAY("<b>###=-</b>")
)
/obj/structure/closet/flock/try_flock_convert(datum/flock/flock, force)
return
@@ -0,0 +1,166 @@
/obj/structure/flock/collector
name = "pulsing node"
desc = "Seems to be pulsing."
icon_state = "collector"
flock_desc = "Provides compute power and charges a nearby APC based on the number of Flock floor tiles it is connected to."
flock_id = "Collector"
max_integrity = 60
resource_cost = 200
/// The collection range.
var/max_range = 4
/// How much power each flocktile grants, percentage of a cell's max charge.
var/power_per_tile = 5
/// All flockturfs we're connected to.
var/tmp/list/turf/simulated/floor/flock/connected_flockturfs = list()
/// All turfs nearby we're tracking for state changes.
var/tmp/list/turf/tracked_turfs = list()
/// Set to TRUE when the connected_turfs list needs an update. Prevents update_connections() being called 20 times during explosions.
var/tmp/need_turfs_update = FALSE
var/cycle_interval = 20 SECONDS
var/tmp/charge_per_cycle = 0
COOLDOWN_DECLARE(charge_cd)
/obj/structure/flock/collector/Initialize(mapload, datum/flock/join_flock)
. = ..()
START_PROCESSING(SSobj, src)
update_connections()
info_tag.set_text("Bandwidth Provided: [bandwidth_provided]")
AddComponent(/datum/component/flock_protection)
/obj/structure/flock/collector/Destroy()
remove_flockturfs(connected_flockturfs)
remove_tracked_turfs(tracked_turfs)
return ..()
/obj/structure/flock/collector/update_icon_state()
icon_state = length(connected_flockturfs) ? "collectoron" : "collector"
return ..()
/obj/structure/flock/collector/Moved(atom/old_loc, movement_dir, forced, list/old_locs, momentum_change)
. = ..()
update_connections()
/obj/structure/flock/collector/flock_structure_examine(mob/user)
var/obj/machinery/power/apc/apc = get_apc()
return list(
SPAN_FLOCKSAY("<b>Connections:</b> Connected to [length(connected_flockturfs)] tile\s."),
SPAN_FLOCKSAY("<b>Bandwidth Provided:</b> [bandwidth_provided]."),
SPAN_FLOCKSAY("<b>APC Connected:</b> [(!apc?.cell || apc.cell.charge >= apc.cell.maxcharge) ? "Not charging." : "Charging local APC at [charge_per_cycle]% every [cycle_interval / 10] seconds."]")
)
/obj/structure/flock/collector/process()
if(need_turfs_update)
update_connections()
var/new_bandwidth = length(connected_flockturfs) * power_per_tile
charge_per_cycle = length(connected_flockturfs) / 4
if(new_bandwidth != bandwidth_provided)
flock.remove_bandwidth_influence(bandwidth_provided)
bandwidth_provided = new_bandwidth
flock.add_bandwidth_influence(bandwidth_provided)
info_tag.set_text("Bandwidth Provided: [bandwidth_provided]")
if(!COOLDOWN_FINISHED(src, charge_cd))
return
COOLDOWN_START(src, charge_cd, cycle_interval)
var/obj/machinery/power/apc/apc = get_apc()
if(isnull(apc))
return
apc.cell?.give(charge_per_cycle / 100 * apc.cell.maxcharge)
apc.AddComponent(/datum/component/flock_ping/apc_power)
/obj/structure/flock/collector/proc/get_apc() as /obj/machinery/power/apc
var/area/A = get_area(src)
var/obj/machinery/power/apc/apc
if(A.apc)
apc = A.apc[0]
return apc
/// Recalculate the turf connections and tracking.
/obj/structure/flock/collector/proc/update_connections()
var/list/old_tracked_turfs = tracked_turfs
var/list/old_flockturfs = connected_flockturfs
var/turf/our_turf = get_turf(src)
var/list/considered_allturfs = list(our_turf)
var/list/considered_flockturfs = list()
if(istype(our_turf, /turf/simulated/floor/flock))
considered_flockturfs += our_turf
for(var/direction in GLOB.cardinal)
var/turf/simulated/floor/flock/iter_turf = our_turf
var/keep_flockturfs = isflockturf(our_turf)
for(var/i in 1 to max_range)
iter_turf = get_step(iter_turf, direction)
if(keep_flockturfs && (!istype(iter_turf, /turf/simulated/floor/flock) || iter_turf.broken))
keep_flockturfs = FALSE
if(keep_flockturfs)
considered_flockturfs += iter_turf
considered_allturfs += iter_turf
var/list/new_tracked_turfs = considered_allturfs - old_tracked_turfs
var/list/removed_tracked_turfs = old_tracked_turfs - considered_allturfs
remove_tracked_turfs(removed_tracked_turfs)
add_tracked_turfs(new_tracked_turfs)
var/list/new_flockturfs = considered_flockturfs - old_flockturfs
var/list/removed_flockturfs = old_flockturfs - considered_flockturfs
remove_flockturfs(removed_flockturfs)
add_flockturfs(new_flockturfs)
need_turfs_update = FALSE
update_appearance(UPDATE_ICON_STATE)
/// Adds turfs to the flockturfs list.
/obj/structure/flock/collector/proc/add_flockturfs(list/add)
for(var/turf/simulated/floor/flock/flockturf as anything in add)
LAZYADD(flockturf.connected_pylons, src)
flockturf.update_power()
connected_flockturfs += add
/// Remove turfs from the flockturfs list.
/obj/structure/flock/collector/proc/remove_flockturfs(list/remove)
for(var/turf/simulated/floor/flock/flockturf as anything in remove)
LAZYREMOVE(flockturf.connected_pylons, src)
flockturf.update_power()
tracked_turfs -= remove
/// Add turfs to the tracked list.
/obj/structure/flock/collector/proc/add_tracked_turfs(list/add)
for(var/turf/T as anything in add)
RegisterSignal(T, COMSIG_TURF_CHANGE, PROC_REF(on_tracked_turf_change))
tracked_turfs += add
/// Remove turfs from the tracked list.
/obj/structure/flock/collector/proc/remove_tracked_turfs(list/remove)
for(var/turf/T as anything in remove)
UnregisterSignal(T, COMSIG_TURF_CHANGE)
tracked_turfs -= remove
/// Called when any of the connected turfs is changed.
/obj/structure/flock/collector/proc/on_tracked_turf_change(datum/source, path, list/new_baseturfs, flags, list/post_change_callbacks)
SIGNAL_HANDLER
need_turfs_update = TRUE
@@ -0,0 +1,37 @@
/obj/structure/flock/compute
name = "strange computer"
desc = "It almost looks like a terminal of some kind."
flock_desc = "A computing node that provides bandwidth to the Flock."
flock_id = "Compute Node"
max_integrity = 60
icon_state = "compute"
bandwidth_provided = 30
/obj/structure/flock/compute/Initialize(mapload, datum/flock/join_flock)
. = ..()
light_range = 1
light_color = "#7BFFFFa2"
light_power = 0.3
update_appearance()
/obj/structure/flock/compute/update_overlays()
. = ..()
overlays.Cut()
var/image/screen = image(icon, "compute_screen")
screen.pixel_y = 16
. += screen
var/image/display = image(icon, "compute_display[rand(1, 9)]")
display.pixel_y = 19
. += display
/obj/structure/flock/compute/flock_structure_examine(mob/user)
return list(
SPAN_FLOCKSAY("<b>Bandwidth Provided:</b> [bandwidth_provided].")
)
/obj/structure/flock/compute/update_info_tag()
info_tag?.set_text("Bandwidth: [bandwidth_provided]")
@@ -0,0 +1,84 @@
/obj/machinery/door/flock
icon = 'icons/goonstation/mob/featherzone.dmi'
name = "imposing wall"
desc = "It sounds like it's hollow."
max_integrity = 200
armor = list(MELEE = -20, BULLET = -20, LASER = 80, ENERGY = 80, BOMB = 0, RAD = 100, FIRE = 80, ACID = 100)
autoclose = TRUE
auto_close_time = 5 SECONDS
safe = FALSE
/obj/machinery/door/flock/Initialize(mapload)
. = ..()
ADD_TRAIT(src, TRAIT_FLOCK_EXAMINE, INNATE_TRAIT)
ADD_TRAIT(src, TRAIT_FLOCK_THING, INNATE_TRAIT)
AddComponent(/datum/component/flock_object)
AddComponent(/datum/component/flock_protection, report_unarmed=FALSE)
/obj/machinery/door/flock/get_flock_id()
return "Solid seal aperature"
/obj/machinery/door/flock/default_deconstruction_screwdriver(mob/user, icon_state_open, icon_state_closed, obj/item/screwdriver)
return FALSE
/obj/machinery/door/flock/allowed(mob/M)
return isflockmob(M)
/obj/machinery/door/flock/CanPathfindPass(to_dir, datum/can_pass_info/pass_info)
var/atom/M = locateUID(pass_info.caller_uid)
if(!M)
return ..()
if(isflockmob(M))
return TRUE
return ..()
/obj/machinery/door/flock/crush()
for(var/turf/T in get_airlock_turfs())
for(var/mob/living/L in T)
if(isflockmob(L))
continue
L.visible_message(SPAN_WARNING("[src] closes on [L], crushing [L.p_them()]!"), SPAN_USERDANGER("[src] closes on you and crushes you!"))
if(isalien(L)) // For xenos
L.adjustBruteLoss(DOOR_CRUSH_DAMAGE * 1.5) // Xenos go into crit after aproximately the same amount of crushes as humans.
L.emote("roar")
else if(ishuman(L)) // For humans
L.adjustBruteLoss(DOOR_CRUSH_DAMAGE)
if(L.stat == CONSCIOUS)
L.emote("scream")
L.Weaken(10 SECONDS)
else // for simple_animals & borgs
L.adjustBruteLoss(DOOR_CRUSH_DAMAGE)
L.add_splatter_floor(T)
for(var/obj/mecha/M in T)
M.take_damage(DOOR_CRUSH_DAMAGE)
/obj/machinery/door/flock/deconstruct(disassembled)
. = ..()
var/turf/T = get_turf(src)
if(disassembled)
new /obj/item/stack/sheet/gnesis(T, 2)
return
new /obj/item/stack/sheet/gnesis(T, 1)
/obj/machinery/door/flock/do_animate(animation)
. = ..()
if(animation == "deny")
playsound(src, 'sound/goonstation/flockmind/flockdrone_door_deny.ogg', 50, TRUE, -2)
/obj/machinery/door/flock/try_flock_convert(datum/flock/flock, force)
return
/obj/machinery/door/flock/examine(mob/user)
if(!isflockmob(user))
return ..()
. = list(
SPAN_FLOCKSAY("<b>###=- Ident confirmed, data packet received.</b>"),
SPAN_FLOCKSAY("<b>ID:</b> [get_flock_id()]"),
SPAN_FLOCKSAY("<b>System Integrity:</b> [get_integrity_percentage()]%"),
SPAN_FLOCKSAY("<b>###=-</b>"),
)
if(stat & BROKEN)
. += SPAN_FLOCKSAY("<b>FUNCTION CRITICALLY IMPAIRED, REPAIRS REQUIRED</>")
. += SPAN_FLOCKSAY("<b>###=-</b>")
@@ -0,0 +1,35 @@
/obj/structure/flock/egg
icon_state = "egg"
name = "glowing orb"
desc = "Some sort of small machine. It looks like its getting ready for something."
flock_desc = "Will soon hatch into a Flockdrone."
anchored = FALSE
density = FALSE
max_integrity = 30
flock_id = "Second-Stage Assembler"
build_time = 6 SECONDS
no_flock_decon = TRUE
/obj/structure/flock/egg/finish_building()
. = ..()
AddComponent(/datum/component/flock_protection)
spawn_mobs()
qdel(src)
/obj/structure/flock/egg/update_info_tag()
info_tag.set_text("Hatch Time: [build_time_left()] seconds")
/obj/structure/flock/egg/proc/spawn_mobs()
new /mob/living/basic/flock/drone(get_turf(src), flock)
/obj/structure/flock/egg/bit
flock_id = "Secondary Second-Stage Assembler"
flock_desc = "Will soon hatch into Flockbits."
/obj/structure/flock/egg/bit/spawn_mobs()
for(var/i in 1 to 3)
new /mob/living/basic/flock/bit(get_turf(src), flock)
@@ -0,0 +1,92 @@
/obj/structure/flock/fabricator
name = "angled pedestal"
desc = "A strange machine."
flock_id = "Fabricator"
flock_desc = "A converter that turns its contents into substrate cubes."
icon_state = "reclaimer"
max_integrity = 20
var/production_interval = 10 SECONDS
var/substrate_per_interval = 50
var/tmp/datum/point_holder/substrate_remaining
var/tmp/timer_id
/obj/structure/flock/fabricator/Initialize(mapload, datum/flock/join_flock)
. = ..()
substrate_remaining = new()
timer_id = addtimer(CALLBACK(src, PROC_REF(produce)), production_interval, TIMER_STOPPABLE | TIMER_LOOP | TIMER_DELETE_ME)
/obj/structure/flock/fabricator/Destroy()
QDEL_NULL(substrate_remaining)
deltimer(timer_id)
return ..()
/obj/structure/flock/fabricator/update_icon_state()
if(substrate_remaining.has_points())
icon_state = "reclaimer"
else
icon_state = "reclaimer-off"
. = ..()
/obj/structure/flock/fabricator/flock_structure_examine(mob/user)
. = ..()
. += SPAN_FLOCKSAY("<b>Substrate remaining:</b> [substrate_remaining.has_points()].")
/obj/structure/flock/fabricator/update_info_tag()
info_tag.set_text("Substrate remaining: [substrate_remaining.has_points()]")
/// Called every 10 seconds via looping timer until there's no substrate left.
/obj/structure/flock/fabricator/proc/produce()
if(substrate_remaining.has_points())
var/obj/item/flock_cube/cube = new(drop_location())
cube.substrate = min(substrate_remaining.has_points(), substrate_per_interval)
substrate_remaining.remove_points(cube.substrate)
if(!substrate_remaining.has_points())
stop_producing()
update_info_tag()
/obj/structure/flock/fabricator/proc/stop_producing()
deltimer(timer_id)
timer_id = null
if(QDELING(src))
return
flock_talk(src, "ALERT: No substrate remaining.", flock, involuntary = TRUE)
update_appearance(UPDATE_ICON_STATE)
//
// All of the try_flock_convert shit
//
/obj/machinery/economy/vending/try_flock_convert(datum/flock/flock, force)
var/substrate
for(var/datum/data/vending_product/product as anything in product_records)
substrate += 3 * product.get_amount_left()
if(!substrate)
qdel(src)
return
var/obj/structure/flock/fabricator/fab = new(get_turf(src), flock)
fab.substrate_remaining.add_points(substrate)
fab.update_info_tag()
update_appearance(UPDATE_ICON_STATE)
qdel(src)
/obj/structure/reagent_dispensers/try_flock_convert(datum/flock/flock, force)
var/substrate = reagents.total_volume / 10 // 100 substrate for a full tank of 1000u
if(!substrate)
qdel(src)
return
var/obj/structure/flock/fabricator/fab = new(get_turf(src), flock)
fab.substrate_remaining.add_points(substrate)
fab.update_info_tag()
update_appearance(UPDATE_ICON_STATE)
qdel(src)
@@ -0,0 +1,155 @@
/obj/structure/grille/flock
desc = "A robust framework of gnesis tubules."
icon = 'icons/goonstation/mob/featherzone.dmi'
icon_state = "barricade-0"
armor = list(MELEE = -20, BULLET = -20, LASER = 80, ENERGY = 80, BOMB = 0, RAD = 100, FIRE = 80, ACID = 100)
pass_flags = PASSFLOCK
flags = CONDUCT | NODECONSTRUCT
rods_type = null
rods_amount = 0
rods_broken = 0
broken_type = /obj/structure/grille/flock/broken
/obj/structure/grille/flock/Initialize(mapload)
. = ..()
var/static/list/loc_connections = list(
COMSIG_ATOM_ENTERED = PROC_REF(on_crossed),
)
AddElement(/datum/element/connect_loc, loc_connections)
AddComponent(/datum/component/flock_protection, FALSE, TRUE, FALSE, FALSE)
ADD_TRAIT(src, TRAIT_FLOCK_EXAMINE, INNATE_TRAIT)
ADD_TRAIT(src, TRAIT_FLOCK_THING, INNATE_TRAIT)
/obj/structure/grille/flock/examine(mob/user)
if(!isflockmob(user))
return ..()
. = list(
SPAN_FLOCKSAY("<b>###=- Ident confirmed, data packet received.</b>"),
SPAN_FLOCKSAY("<b>ID:</b> Barricade"),
SPAN_FLOCKSAY("<b>System Integrity:</b> [get_integrity_percentage()]%"),
SPAN_FLOCKSAY("<b>###=-</b>")
)
/obj/structure/grille/flock/take_damage(damage_amount, damage_type = BRUTE, damage_flag = 0, sound_effect = 1, attack_dir)
. = ..()
update_icon(UPDATE_ICON_STATE)
/obj/structure/grille/flock/update_icon_state()
if(QDELETED(src) || broken)
return
var/ratio = obj_integrity / max_integrity
if(ratio > 0.5)
return
icon_state = "barricade-[rand(1,3)]"
/obj/structure/grille/flock/Bumped(atom/user)
if(isliving(user))
if(!(shockcooldown <= world.time))
return
if(!anchored || broken) // unanchored/broken grilles are never connected
return
if(!prob(70))
return
if(!in_range(src, user))// To prevent TK and mech users from getting shocked
return
var/mob/living/M = user
var/siemens = 1
if(ishuman(M))
var/mob/living/carbon/human/H = M
if(H.gloves)
var/obj/item/clothing/gloves/G = H.gloves
siemens = G.siemens_coefficient
M.electrocute_act(30, src, siemens)
shockcooldown = world.time + my_shockcooldown
/obj/structure/grille/flock/temperature_expose(exposed_temperature, exposed_volume)
if(!broken)
if(exposed_temperature > T0C + 32000)
take_damage(1, BURN, 0, 0)
/obj/structure/grille/flock/shock(mob/user, prb)
if(!anchored || broken) // unanchored/broken grilles are never connected
return FALSE
if(!prob(prb))
return FALSE
if(!in_range(src, user))// To prevent TK and mech users from getting shocked
return FALSE
if(isflockmob(user))
return FALSE
var/mob/living/M = user
var/siemens = 1
if(ishuman(M))
var/mob/living/carbon/human/H = M
if(H.gloves)
var/obj/item/clothing/gloves/G = H.gloves
siemens = G.siemens_coefficient
M.electrocute_act(20, src, siemens)
do_sparks(3, 1, src)
return TRUE
/obj/structure/grille/flock/hitby(atom/movable/AM, skipcatch, hitpush, blocked, datum/thrownthing/throwingdatum)
if(isobj(AM))
if(prob(50) && anchored && !broken)
var/obj/O = AM
if(O.throwforce != 0)// don't want to let people spam tesla bolts, this way it will break after time
playsound(src, 'sound/magic/lightningshock.ogg', 100, TRUE, extrarange = 5)
tesla_zap(src, 3, 12000, ZAP_MOB_DAMAGE | ZAP_OBJ_DAMAGE | ZAP_MOB_STUN | ZAP_ALLOW_DUPLICATES)
return ..()
/obj/structure/grille/flock/obj_break()
if(!broken)
new broken_type(loc)
qdel(src)
/obj/structure/grille/flock/proc/on_crossed(atom/source, atom/movable/crosser)
SIGNAL_HANDLER
if(!isflockdrone(crosser) || broken)
return
if(!HAS_TRAIT(crosser, TRAIT_FLOCKPHASE))
animate_flockpass(crosser)
/obj/structure/grille/flock/CanPass(atom/movable/mover, border_dir)
. = ..()
if(.)
return .
if(!isflockdrone(mover))
return .
var/mob/living/basic/flock/drone/bird = mover
if(HAS_TRAIT(bird, TRAIT_FLOCKPHASE))
return TRUE
if(bird.can_flockphase())
return TRUE
/obj/structure/grille/flock/CanPathfindPass(to_dir, datum/can_pass_info/pass_info)
var/atom/M = locateUID(pass_info.caller_uid)
if(!M)
return ..()
if(!isflockdrone(M))
return ..()
var/mob/living/basic/flock/drone/bird = M
if(!bird.can_flockphase())
return ..()
return TRUE
/obj/structure/grille/flock/broken
desc = "A broken framework of gnesis tubules."
icon_state = "barricade-cut"
armor = list(MELEE = -20, BULLET = -20, LASER = 80, ENERGY = 80, BOMB = 0, RAD = 100, FIRE = 80, ACID = 100)
density = FALSE
obj_integrity = 20
broken = TRUE
grille_type = /obj/structure/grille/flock
broken_type = null
/obj/structure/grille/flock/try_flock_convert(datum/flock/flock, force)
return
@@ -0,0 +1,100 @@
/obj/structure/flock/interceptor
name = "glowing fountain"
desc = "Some kind of fountain. The fluid inside ripples with energy."
icon_state = "interceptor-off"
flock_desc = "A defense turret that fires high speed gnesis bolts at nearby projectiles, annihilating them."
flock_id = "Interceptor"
resource_cost = 100
var/tmp/datum/proximity_monitor/advanced/interceptor/proxmon
/// How long it takes to recharge
var/recharge_time = 10 SECONDS
/// Is it charged?
var/tmp/is_charged = FALSE
/// world.time the interceptor will be charged (might be off by 1 sometimes? idk timers wierd)
var/tmp/ready_time
/// timer id for recharging
var/tmp/recharge_timer_id
/obj/structure/flock/interceptor/Initialize(mapload, datum/flock/join_flock)
. = ..()
proxmon = new(src, 3, TRUE)
process()
AddComponent(/datum/component/flock_protection)
/obj/structure/flock/interceptor/Destroy()
QDEL_NULL(proxmon)
deltimer(recharge_timer_id)
return ..()
/obj/structure/flock/interceptor/update_icon_state()
if(!active)
icon_state = "interceptor-off"
else if(is_charged)
icon_state = "interceptor-ready"
else
icon_state = "interceptor-generating"
. = ..()
/obj/structure/flock/interceptor/update_info_tag()
info_tag.set_text(get_status())
/obj/structure/flock/interceptor/flock_structure_examine(mob/user)
. = ..()
. += "[get_status()]."
/obj/structure/flock/interceptor/process()
if(active)
if(flock.available_bandwidth() < 0)
set_active(FALSE)
else
if(flock.can_afford(active_bandwidth_cost))
set_active(TRUE)
/obj/structure/flock/interceptor/set_active(new_state)
. = ..()
if(isnull(.))
return
if(. == TRUE)
begin_charging()
else
is_charged = FALSE
deltimer(recharge_timer_id)
/obj/structure/flock/interceptor/proc/begin_charging()
is_charged = FALSE
recharge_timer_id = addtimer(CALLBACK(src, PROC_REF(ready_to_fire)), recharge_time, TIMER_DELETE_ME | TIMER_STOPPABLE)
update_appearance()
/// Called when recharge_cd is ready to fire
/obj/structure/flock/interceptor/proc/ready_to_fire()
is_charged = TRUE
update_appearance()
/obj/structure/flock/interceptor/proc/get_status()
if(!active)
return "Idle"
if(is_charged)
return "Ready"
return "Recharging."
/// Called when a projectile enters the view of the interceptor.
/obj/structure/flock/interceptor/proc/try_intercept_projectile(obj/projectile/P)
if(!is_charged)
return FALSE
if(!istype(P, /obj/projectile/bullet))
return FALSE
playsound(src, 'sound/goonstation/flockmind/flockdrone_fart.ogg', 50, TRUE) // It honestly kind of fits??
Beam(get_turf(P), icon_state = "rped_upgrade", icon = 'icons/effects/effects.dmi', time = 0.5 SECONDS)
qdel(P)
begin_charging()
return TRUE
@@ -0,0 +1,28 @@
/obj/structure/lattice/flock
name = "fibrenet"
desc = "A floating lattice of lightweight gnesis."
icon = 'icons/goonstation/mob/featherzone.dmi'
icon_state = "fibrenet"
base_icon_state = "fibrenet"
armor = list(MELEE = 20, BULLET = 10, LASER = 10, ENERGY = 0, BOMB = 10, RAD = 0, FIRE = 70, ACID = 60)
max_integrity = 30
/obj/structure/lattice/flock/Initialize(mapload)
. = ..()
AddComponent(/datum/component/flock_protection, FALSE, TRUE, FALSE, FALSE)
ADD_TRAIT(src, TRAIT_FLOCK_EXAMINE, INNATE_TRAIT)
ADD_TRAIT(src, TRAIT_FLOCK_THING, INNATE_TRAIT)
/obj/structure/lattice/flock/examine(mob/user)
if(!isflockmob(user))
return ..()
. = list(
SPAN_FLOCKSAY("<b>###=- Ident confirmed, data packet received.</b>"),
SPAN_FLOCKSAY("<b>ID:</b> Structural Foundation"),
SPAN_FLOCKSAY("<b>System Integrity:</b> [get_integrity_percentage()]%"),
SPAN_FLOCKSAY("<b>###=-</b>")
)
/obj/structure/lattice/flock/try_flock_convert(datum/flock/flock, force)
return
@@ -0,0 +1,35 @@
/obj/machinery/light/floor/has_bulb/flock
icon = 'icons/goonstation/objects/lighting.dmi'
icon_state = "flock_floor"
color = "#72bfac"
light_color = "#72bfac"
brightness_color = "#72bfac"
base_state = "flock_floor"
glow_icon = 'icons/goonstation/objects/lighting.dmi'
glow_icon_state = "flock_floor"
nightshift_allowed = FALSE
no_emergency = TRUE
/obj/machinery/light/floor/has_bulb/flock/try_flock_convert(datum/flock/flock, force)
return
/obj/machinery/light/floor/has_bulb/get_flock_id()
return "Light emitter"
/obj/machinery/light/flock
icon = 'icons/goonstation/objects/lighting.dmi'
icon_state = "flock"
color = "#72bfac"
light_color = "#72bfac"
brightness_color = "#72bfac"
base_state = "flock"
glow_icon = 'icons/goonstation/objects/lighting.dmi'
glow_icon_state = "flock"
nightshift_allowed = FALSE
no_emergency = TRUE
/obj/machinery/light/flock/try_flock_convert(datum/flock/flock, force)
return
/obj/machinery/light/flock/get_flock_id()
return "Light emitter"
@@ -0,0 +1,228 @@
/obj/structure/flock/relay
icon = 'icons/goonstation/objects/featherzone-160x160.dmi'
icon_state = "structure-relay"
name = "titanic polyhedron"
desc = "The sight of the towering geodesic sphere fills you with dread. A thousand voices whisper to you."
pixel_x = -64
pixel_y = -64
flock_desc = "Your goal and purpose. Defend it until it can broadcast the Signal."
flock_id = "Signal Relay Broadcast Amplifier"
max_integrity = 500
move_resist = MOVE_FORCE_OVERPOWERING
resource_cost = 750
allow_flockpass = FALSE
no_flock_decon = TRUE
/// The object showing admins/dchat the time left
var/obj/effect/countdown/flock/counter
/// Background noise
var/datum/looping_sound/flock_relay/soundloop
/// Time the structure started.
var/tmp/started_time
/// How long it takes until the signal is broadcast and the flock wins :D
var/tmp/win_time = 360 SECONDS
var/tmp/flock_won_da_game = FALSE
/// Radius for turf conversion. Automatically increments during processing.
var/tmp/conversion_radius = 3
var/tmp/list/turfs_to_convert = list()
/obj/structure/flock/relay/Initialize(mapload, datum/flock/join_flock)
. = ..()
for(var/turf/simulated/T in range(3, src))
T.ChangeTurf(/turf/simulated/floor/flock)
flock.claim_turf(T)
for(var/obj/structure/S in orange(3, src))
if(!(S.flags & INDESTRUCTIBLE) && !istype(S, /obj/structure/flock) && !istype(S, /obj/machinery/atmospherics) && !istype(S, /obj/structure/cable))
S.Destroy()
for(var/obj/machinery/MA in orange(3, src))
if(!(MA.flags & INDESTRUCTIBLE))
MA.Destroy()
for(var/obj/item/I in range(2, src))
var/turf/T = get_edge_target_turf(src, get_dir(src, I))
I.throw_at(T, 3, 1)
for(var/mob/M in range(2, src))
var/turf/T = get_edge_target_turf(src, get_dir(src, M))
M.throw_at(T, 3, 1)
AddComponent(/datum/component/multitile, list(
list(
list(1, 1, 1, 1, 1),
list(1, 1, 1, 1, 1),
list(1, 1, MACH_CENTER, 1, 1),
list(1, 1, 1, 1, 1),
list(1, 1, 1, 1, 1)
)
))
AddComponent(/datum/component/flock_protection)
soundloop = new(get_turf(src), start_immediately = TRUE)
started_time = world.time
flock.built_relay = src
flock.set_flock_game_status(FLOCK_ENDGAME_RELAY_BUILT)
log_game("The Flock ([flock?.name || "NULL"]) has constructed a relay at [AREACOORD(src)].")
message_admins("The Flock has constructed the relay at [ADMIN_VERBOSEJMP(src)].")
SSshuttle.registerHostileEnvironment(src, FALSE)
to_chat(
flock.overmind,
SPAN_FLOCKSAY(SPAN_BIG("You pool the collective processing power of The Flock to transmit The Signal. If the relay is destroyed, so too will be The Flock!"))
)
flock_talk(null, "THE RELAY HAS BEEN CONSTRUCTED! DEFEND IT AT ALL COSTS! BRING FORTH THE FULL BREADTH OF THE DIVINE FLOCK!", flock)
counter = new(src)
counter.start()
addtimer(CALLBACK(src, PROC_REF(announce_relay)), 10 SECONDS)
START_PROCESSING(SSprocessing, src)
/obj/structure/flock/relay/Destroy()
SSshuttle.clearHostileEnvironment(src)
STOP_PROCESSING(SSprocessing, src)
if(flock)
flock.free_structure(src)
flock.game_over(completely_destroy = TRUE)
turfs_to_convert = null
qdel(soundloop)
qdel(counter)
return ..()
/obj/structure/flock/relay/take_damage(damage_amount, damage_type, damage_flag, sound_effect, attack_dir, armor_penetration, allow_break)
. = ..()
if(. >= 5)
var/alpha_min = clamp(255 - . * 6, 100, 255)
animate(src, time = 0.1 SECONDS, color = list(1.5,0,0,0, 0,1.5,0,0, 0,0,1.5,0, 0,0,0,alpha_min/255, 0,0,0,0))
animate(time = 0.3 SECONDS, color = null, alpha = 255)
/obj/structure/flock/relay/examine(mob/user)
. = ..()
if(flock_won_da_game && !isflockmob(user))
. += SPAN_FLOCKSAY("Your life flashes before your eyes.")
/obj/structure/flock/relay/update_overlays()
. = ..()
overlays.Cut()
if(flock_won_da_game)
. += "structure-relay-sparks"
/obj/structure/flock/relay/flock_structure_examine(mob/user)
var/timeleft = (started_time + win_time - world.time) / 10
if(timeleft)
return list(
SPAN_FLOCKSAY("<b>Broadcast In:</b> [(started_time + win_time - world.time) / 10] second\s.")
)
else
return list(
SPAN_FLOCKSAY("<b><i>BROADCASTING IN PROGRESS.</i></b>")
)
/obj/structure/flock/relay/update_info_tag()
if(!flock_won_da_game)
info_tag?.set_text("Broadcast in: [(started_time + win_time - world.time) / 10] second\s")
else
info_tag?.set_text("Transmitting")
/obj/structure/flock/relay/process()
if(world.time >= (started_time + win_time))
lorimer_burst()
return PROCESS_KILL
update_info_tag()
var/turf/base = get_turf(src)
/// Get a chebyshev ring without gigantic list ops.
if(!length(turfs_to_convert))
for(var/xo in -conversion_radius to conversion_radius)
var/turf/potential_turf = locate(base.x + xo, base.y + conversion_radius, base.z)
if(potential_turf)
turfs_to_convert += potential_turf
potential_turf = locate(base.x - xo, base.y - conversion_radius, base.z)
if(potential_turf)
turfs_to_convert += potential_turf
for(var/yo in -conversion_radius to conversion_radius)
var/turf/potential_turf = locate(base.x + conversion_radius, base.y + yo, base.z)
if(potential_turf)
turfs_to_convert += potential_turf
potential_turf = locate(base.x - conversion_radius, base.y - yo, base.z)
if(potential_turf)
turfs_to_convert += potential_turf
conversion_radius++
// Convert 5 turfs per second.
for(var/i in 1 to min(5, length(turfs_to_convert)))
var/turf/conversion_target = turfs_to_convert[length(turfs_to_convert)]
turfs_to_convert.len--
if(!isflockturf(conversion_target) && !isspaceturf(conversion_target))
flock.claim_turf(conversion_target)
/obj/structure/flock/relay/proc/alert_organics()
for(var/mob/M as anything in GLOB.player_list)
var/turf/T = get_turf(M)
if(is_station_level(T.z) && M.can_hear())
M.playsound_local(M, 'sound/goonstation/flockmind/Flock_Reactor.ogg', 30, FALSE)
to_chat(M, SPAN_FLOCKSAY("<b>A horrible, otherworldly wave eminates from the <i>[dir2text(get_dir(get_turf(M), loc))]</i>."))
/obj/structure/flock/relay/proc/announce_relay()
var/message = Gibberish("The Signal is coming.", 70, replace_rate = 50)
GLOB.major_announcement.Announce(message,
"Hijacked Signal",
'sound/effects/siren-spooky.ogg',
new_sound2 = 'sound/AI/aimalf.ogg'
)
/// GG
/obj/structure/flock/relay/proc/lorimer_burst()
set waitfor = FALSE
flock_won_da_game = TRUE
flock.set_flock_game_status(FLOCK_ENDGAME_RELAY_ACTIVATING)
log_game("The Flock ([flock?.name || "NULL"]) has successfully broadcast The Signal at [AREACOORD(src)].")
update_appearance(UPDATE_OVERLAYS)
flock_talk(null, "!!! TRASMITTING SIGNAL !!!", flock)
visible_message("<span size=5>[gradient_text("[src] begins sparking wildly! The air is charged with static!", "#3cb5a3", "#124e43")]</span>")
soundloop.stop()
qdel(counter)
for(var/mob/M as anything in GLOB.player_list)
if(M.can_hear())
M.playsound_local(M, 'sound/goonstation/flockmind/flock_broadcast_charge.ogg', 30, FALSE)
sleep(20 SECONDS)
for(var/mob/M as anything in GLOB.player_list)
if(M.can_hear())
M.playsound_local(M, 'sound/goonstation/flockmind/flock_broadcast_kaboom.ogg', 30, FALSE)
if(isliving(M))
var/mob/living/L = M
L.flash_eyes(3, TRUE, TRUE, TRUE)
sleep(2 SECONDS)
flock.set_flock_game_status(FLOCK_ENDGAME_VICTORY)
explosion(src, 30, 60, 80, 100, ignorecap = TRUE, cause = src)
sleep(2 SECONDS)
for(var/obj/machinery/telecomms/T in SSmachines.get_by_type(/obj/machinery/telecomms))
T.emp_act(EMP_HEAVY)
SSshuttle.emergency.request(null, 0.3)
SSshuttle.emergency.canRecall = FALSE // Cannot recall
@@ -0,0 +1,63 @@
/obj/structure/flock/rift
name = "static rift"
desc = "That doesn't look human."
flock_desc = "The rift through which your Flock will enter this world."
flock_id = "Entry Rift"
icon_state = "rift"
density = FALSE
max_integrity = 200
build_time = 10 SECONDS
no_flock_decon = TRUE
/obj/structure/flock/rift/Initialize(mapload, datum/flock/join_flock)
. = ..()
var/turf/T = get_turf(src)
playsound(T, 'sound/goonstation/flockmind/ArtifactFea3.ogg', 50, FALSE, use_reverb = FALSE)
/obj/structure/flock/rift/update_info_tag()
info_tag.set_text("Entry Time: [build_time_left()] seconds")
/obj/structure/flock/rift/finish_building()
. = ..()
for(var/i in 1 to 4)
new /obj/structure/flock/egg(src, flock)
// 1 flockbit
new /obj/structure/flock/egg/bit(src, flock)
var/list/convertable_turfs = list()
for(var/turf/simulated/T in RANGE_TURFS(2, src))
if(!isflockturf(T) && T.can_flock_convert())
convertable_turfs += T
shuffle_inplace(convertable_turfs)
convertable_turfs.len = min(convertable_turfs.len, 16)
// Convert turfs
for(var/turf/simulated/T as anything in convertable_turfs)
if(flock)
flock.claim_turf(T)
else
flock_convert_turf(T)
// Spawn sentinels
var/sentinel_count = 0
for(var/turf/simulated/floor/flock/T in convertable_turfs)
if(!T.can_flock_occupy())
continue
new /obj/structure/flock/sentinel(T, flock)
sentinel_count++
if(sentinel_count == 2)
break
// Spread contents
var/turf/turfloc = get_turf(src)
for(var/atom/movable/AM as anything in src)
AM.forceMove(turfloc)
AM.throw_at(get_random_perimeter_turf(turfloc, 10), 10, 3, spin = FALSE)
qdel(src)
@@ -0,0 +1,177 @@
#define NOT_CHARGED -1
#define LOSING_CHARGE 0
#define CHARGING 1
#define CHARGED 2
/obj/structure/flock/sentinel
name = "glowing pylon"
desc = "A glowing pylon of sorts, faint sparks are jumping inside of it."
flock_desc = "A charged pylon, capable of sending disorienting arcs of electricity at enemies. Consumes 20 compute."
icon_state = "sentinel"
max_integrity = 80
flock_id = "Sentinel"
active_bandwidth_cost = 20
resource_cost = 150
/// Attacks require charging
var/datum/point_holder/charge
/// Charge gained per second while a target is in range
var/charge_per_second = 10
/// Trend of charge.
var/charge_status = NOT_CHARGED
/// Turret range
var/range = 4
/// Damage per zap.
var/damage_per_zap = 5
/obj/structure/flock/sentinel/Initialize(mapload, joinflock)
charge = new
charge.set_max_points(100)
. = ..()
AddComponent(/datum/component/flock_protection)
START_PROCESSING(SSobj, src)
/obj/structure/flock/sentinel/Destroy(force)
STOP_PROCESSING(SSobj, src)
QDEL_NULL(charge)
return ..()
/obj/structure/flock/sentinel/process(seconds_per_tick)
if(isnull(flock))
set_active(FALSE)
return PROCESS_KILL
// Check if the flock can continue to run the sentinel
if(active)
if(flock.available_bandwidth() < 0)
set_active(FALSE)
else
if(flock.can_afford(active_bandwidth_cost))
set_active(TRUE)
if(!active)
if(charge.has_points())
charge.adjust_points((charge_per_second / 2) * seconds_per_tick)
update_info_tag()
charge_status = LOSING_CHARGE
else
charge_status = NOT_CHARGED
update_appearance(UPDATE_ICON_STATE)
return
// Gain more charge
if(charge_status != CHARGED)
charge.adjust_points(charge_per_second * seconds_per_tick)
update_info_tag()
if(charge.has_points(100))
charge_status = CHARGED
update_appearance(UPDATE_ICON_STATE)
else if(charge_status != CHARGING)
charge_status = CHARGING
update_appearance(UPDATE_ICON_STATE)
return
// If the flock has no enemies, don't bother getting a target.
if(!length(flock.enemies))
return
// Select target
var/mob/living/target
for(var/mob/living/L in viewers(range, src))
if(HAS_TRAIT(L, TRAIT_SHOCKED_BY_SENTINEL) || !flock.is_mob_enemy(L))
continue
if(L.stat != CONSCIOUS || L.incapacitated(ignore_restraints = TRUE, ignore_grab = TRUE))
continue
target = L
break
// No target, abort.
if(isnull(target))
return
charge_status = CHARGING
charge.adjust_points(-100)
update_info_tag()
target.electrocute_act(30, src, 1)
addtimer(CALLBACK(src, PROC_REF(clear_trait), target), 2 SECONDS)
Beam(target, "lightning[rand(1, 12)]", 'icons/effects/effects.dmi', 0.5 SECONDS, INFINITY)
playsound(loc, 'sound/magic/lightningbolt.ogg', 60, TRUE)
target.visible_message(
SPAN_DANGER("<b>[target]</b> is struck by a bolt of energy arcing off of <b>[src]</b>."),
blind_message = SPAN_HEAR("You hear a loud electrical crackle."),
)
var/list/hit_mobs = list(target)
var/mob/previous_hit = target
var/end_of_chain = TRUE
for(var/i in 1 to 3)
end_of_chain = TRUE
for(var/mob/living/M in viewers(2, get_turf(previous_hit)))
if((M in hit_mobs) || !flock.is_mob_enemy(M))
continue
if(M.stat == DEAD || M.incapacitated(ignore_restraints = TRUE, ignore_grab = TRUE))
continue
end_of_chain = FALSE
hit_mobs += M
M.electrocute_act(20, src, 0)
previous_hit.Beam(M, "lightning[rand(1, 12)]", 'icons/effects/effects.dmi', 0.5 SECONDS, INFINITY)
target.visible_message(
SPAN_DANGER("<b>[M]</b> is struck by a bolt of energy arcing off of <b>[previous_hit]</b>."),
blind_message = SPAN_HEAR("You hear a loud electrical crackle."),
)
previous_hit = M
log_attack(src, target, "damages with an arc chain owned by [flock.name]")
break
if(end_of_chain)
break
/obj/structure/flock/sentinel/update_icon_state()
if(!active || charge_status == NOT_CHARGED)
icon_state = "sentinel"
else
icon_state = "sentinelon"
return ..()
/obj/structure/flock/sentinel/flock_structure_examine(mob/user)
var/charge_status_str
switch(charge_status)
if(NOT_CHARGED)
charge_status_str = "Idle"
if(LOSING_CHARGE)
charge_status_str = "Losing charge"
if(CHARGING)
charge_status_str = "Charging"
if(CHARGED)
charge_status_str = "Charged"
return list(
SPAN_FLOCKSAY("<b>Status:</b> [charge_status_str]"),
SPAN_FLOCKSAY("<b>Charge Percentage: [charge.has_points()]%"),
)
/obj/structure/flock/sentinel/update_info_tag()
info_tag.set_text("Charge: [charge.has_points()]%")
/obj/structure/flock/sentinel/proc/clear_trait(mob/living/target)
REMOVE_TRAIT(target, TRAIT_SHOCKED_BY_SENTINEL, ref(src))
#undef NOT_CHARGED
#undef LOSING_CHARGE
#undef CHARGING
#undef CHARGED
@@ -0,0 +1,75 @@
/obj/structure/flock/tealprint
name = "building ghost"
desc = "It's some weird looking ghost building. Seems like its under construction, You can see faint strands of material floating in it."
density = FALSE
no_flock_decon = TRUE
flock_id = "Construction Tealprint" // if you change this update Flockpanel.tsx!!!!!
var/datum/point_holder/tealprint/substrate
var/obj/structure/flock/building_type = null
/obj/structure/flock/tealprint/Initialize(mapload, datum/flock/join_flock, obj/structure/flock/desired_type)
. = ..()
var/turf/T = loc
if(!T.can_flock_occupy(src))
return INITIALIZE_HINT_QDEL
building_type = desired_type
icon = initial(building_type.icon)
icon_state = initial(building_type.icon_state)
alpha = 104
substrate = new()
substrate.set_max_points(initial(desired_type.resource_cost))
substrate.owner = src
/obj/structure/flock/tealprint/Destroy()
QDEL_NULL(substrate)
return ..()
/obj/structure/flock/tealprint/update_info_tag()
info_tag?.set_text("Substrate: [substrate.has_points()] / [substrate.get_max_points()]")
/obj/structure/flock/tealprint/flock_structure_examine(mob/user)
return list(
SPAN_FLOCKSAY("<b>Construction Percentage:</b> [floor(substrate.has_points() / substrate.get_max_points() * 100)]"),
SPAN_FLOCKSAY("<b>Construction Progress:</b> [substrate.has_points()] added, [substrate.get_max_points()] needed")
)
/// Complete the structure.
/obj/structure/flock/tealprint/proc/complete_structure()
var/obj/structure/flock/structure = new building_type(get_turf(src))
flock_talk(src, "Tealprint for [structure.flock_id] realized.", flock, involuntary = TRUE)
qdel(src)
/// Attempt to cancel the construction, spitting out the substrate. Some structures cannot be cancelled.
/obj/structure/flock/tealprint/proc/try_cancel_structure()
if(!initial(building_type.cancellable))
return FALSE
if(substrate.has_points())
var/obj/item/flock_cube/cube = new(drop_location())
cube.substrate = substrate.has_points()
flock_talk(src, "Tealprint dematerializing", flock, involuntary = TRUE)
playsound(src, 'sound/goonstation/flockmind/flockdrone_door_deny.ogg', 30, TRUE, extrarange = -10)
qdel(src)
return TRUE
/datum/point_holder/tealprint
var/obj/structure/flock/tealprint/owner
/datum/point_holder/tealprint/Destroy(force, ...)
owner = null
return ..()
/datum/point_holder/tealprint/add_points(num)
. = ..()
if(.)
if(is_full())
owner.complete_structure()
else
owner.update_info_tag()
@@ -0,0 +1,136 @@
/obj/structure/flock/gnesis_turret
name = "fluid vat"
desc = "A vat of bubbling teal fluid, covered in hollow spikes."
flock_desc = "A turret that fires gnesis-filled spikes at enemies, beginning their conversion to Flockbits. Consumes 50 bandwidth passively."
icon_state = "teleblocker-off"
max_integrity = 80
flock_id = "Gnesis turret"
resource_cost = 150
active_bandwidth_cost = 50
var/range = 8
var/projectile_count = 4
var/projectile_type = /obj/projectile/bullet/dart/piercing/gnesis
var/current_target
/obj/structure/flock/gnesis_turret/Initialize(mapload, datum/flock/join_flock)
. = ..()
AddComponent(/datum/component/flock_protection)
START_PROCESSING(SSobj, src)
/obj/structure/flock/gnesis_turret/Destroy()
if(prob(25))
new /obj/item/reagent_containers/glass/gnesis/prefilled(get_turf(src))
set_target(null)
STOP_PROCESSING(SSobj, src)
return ..()
/obj/structure/flock/gnesis_turret/update_icon_state()
if(active)
icon_state = "teleblocker-on"
else
icon_state = "teleblocker-off"
. = ..()
/obj/structure/flock/gnesis_turret/process()
if(isnull(flock))
set_active(FALSE)
return PROCESS_KILL
if(active)
if(flock.available_bandwidth() < 0)
set_active(FALSE)
else
if(flock.can_afford(active_bandwidth_cost))
set_active(TRUE)
if(!active)
return
if(isnull(current_target) || !is_valid_target(current_target))
set_target(find_target())
if(isnull(current_target))
return
fire()
/obj/structure/flock/gnesis_turret/proc/fire(bullets = src.projectile_count)
if(isnull(current_target))
return
var/obj/projectile/boolet = new projectile_type
boolet.preparePixelProjectile(current_target, loc, deviation = rand(-5, 5))
boolet.firer = src
boolet.fire()
bullets--
if(bullets)
addtimer(CALLBACK(src, PROC_REF(fire), bullets), 0.1 SECONDS)
/obj/structure/flock/gnesis_turret/proc/find_target()
var/list/targets = list()
for(var/mob/living/L in viewers(range, src))
if(is_valid_target(L))
targets += L
for(var/obj/mecha/M in viewers(range, src))
targets += M
var/target = null
var/target_dist = INFINITY
for(var/L as anything in targets)
var/dist = get_dist(src, L)
if(dist < target_dist)
target = L
target_dist = dist
return target
/obj/structure/flock/gnesis_turret/proc/is_valid_target(target)
if(!can_see(src, target, range))
return FALSE
if(ismecha(target))
return TRUE
var/mob/living/L = target
if(isflockmob(L))
return FALSE
if(L.stat != CONSCIOUS)
return FALSE
if(!flock.is_mob_enemy(L))
return FALSE
if(L.incapacitated(ignore_restraints = TRUE, ignore_grab = TRUE))
return FALSE
return TRUE
/obj/structure/flock/gnesis_turret/proc/set_target(mob/new_target)
if(current_target)
UnregisterSignal(current_target, COMSIG_PARENT_QDELETING)
current_target = new_target
if(current_target)
RegisterSignal(current_target, COMSIG_PARENT_QDELETING, PROC_REF(target_del))
/obj/structure/flock/gnesis_turret/proc/target_del(datum/source)
SIGNAL_HANDLER
set_target(null)
/obj/projectile/bullet/dart/piercing/gnesis
name = "barbed crystalline spike"
desc = "A hollow teal crystal, like some sort of weird alien syringe. It has a barbed tip. Nasty!"
/obj/projectile/bullet/dart/piercing/gnesis/Initialize(mapload)
. = ..()
reagents.add_reagent(/datum/reagent/gnesis, 2)
@@ -0,0 +1,68 @@
/obj/structure/window/flock
icon = 'icons/goonstation/mob/featherzone.dmi'
pass_flags = PASSFLOCK
heat_resistance = 32000
glass_type = /obj/item/stack/sheet/gnesis_glass
/obj/structure/window/flock/Initialize(mapload, direct)
. = ..()
AddComponent(/datum/component/flock_object)
AddComponent(/datum/component/flock_protection, report_unarmed=FALSE)
var/static/list/loc_connections = list(
COMSIG_ATOM_ENTERED = PROC_REF(on_crossed),
)
AddElement(/datum/element/connect_loc, loc_connections)
ADD_TRAIT(src, TRAIT_FLOCK_THING, INNATE_TRAIT)
/obj/structure/window/flock/proc/on_crossed(atom/source, atom/movable/crosser)
SIGNAL_HANDLER
if(!isflockdrone(crosser))
return
if(!HAS_TRAIT(crosser, TRAIT_FLOCKPHASE))
animate_flockpass(crosser)
/obj/structure/window/flock/examine(mob/user)
if(!isflockmob(user))
return ..()
. = list(
SPAN_FLOCKSAY("<b>###=- Ident confirmed, data packet received.</b>"),
SPAN_FLOCKSAY("<b>ID:</b> Transparent Barrier"),
SPAN_FLOCKSAY("<b>System Integrity:</b> [get_integrity_percentage()]%"),
SPAN_FLOCKSAY("<b>###=-</b>")
)
/obj/structure/window/flock/CanPass(atom/movable/mover, border_dir)
. = ..()
if(.)
return .
if(!isflockdrone(mover))
return .
var/mob/living/basic/flock/drone/bird = mover
if(HAS_TRAIT(bird, TRAIT_FLOCKPHASE))
return TRUE
if(bird.can_flockphase())
return TRUE
/obj/structure/window/flock/CanPathfindPass(to_dir, datum/can_pass_info/pass_info)
var/atom/M = locateUID(pass_info.caller_uid)
if(!M)
return ..()
if(!isflockdrone(M))
return ..()
var/mob/living/basic/flock/drone/bird = M
if(!bird.can_flockphase())
return ..()
return TRUE
/obj/structure/window/flock/fulltile
dir = SOUTHWEST
fulltile = TRUE
/obj/structure/window/flock/try_flock_convert(datum/flock/flock, force)
return
@@ -0,0 +1,25 @@
/datum/ai_planning_subtree/goap/flockdrone
possible_behaviors = list(
/datum/ai_behavior/flock/stare,
/datum/ai_behavior/flock/wander,
/datum/ai_behavior/flock/resist,
/datum/ai_behavior/flock/find_conversion_target,
/datum/ai_behavior/flock/find_deconstruct_target,
/datum/ai_behavior/flock/find_heal_target,
/datum/ai_behavior/flock/find_existing_nest,
/datum/ai_behavior/flock/find_harvest_target,
/datum/ai_behavior/flock/find_closed_container,
/datum/ai_behavior/flock/find_storage_item,
/datum/ai_behavior/flock/find_conversion_target/nest,
/datum/ai_behavior/flock/attack_target,
/datum/ai_behavior/flock/find_capture_target,
/datum/ai_behavior/flock/find_deposit_target,
)
/datum/ai_planning_subtree/goap/flock
possible_behaviors = list(
/datum/ai_behavior/flock/stare,
/datum/ai_behavior/flock/wander,
/datum/ai_behavior/flock/resist,
/datum/ai_behavior/flock/find_conversion_target,
)
@@ -0,0 +1,69 @@
/turf/simulated/floor/flock
name = "weird floor"
icon = 'icons/goonstation/mob/featherzone.dmi'
icon_state = "floor"
base_icon_state = "floor"
light_color = "#19b299"
light_power = 0.4
light_range = 4
var/is_on = FALSE
/// List of mobs flockrunning on this turf, lazylist.
var/list/mob/living/basic/flock/flockrunning_mobs
/// List of pylons powering this turf, lazylist.
var/list/obj/structure/flock/collector/connected_pylons
/turf/simulated/floor/flock/Initialize(mapload)
. = ..()
ADD_TRAIT(src, TRAIT_FLOCK_THING, INNATE_TRAIT)
AddComponent(/datum/component/flock_protection, FALSE, TRUE, FALSE, FALSE)
/turf/simulated/floor/flock/Destroy(force)
REMOVE_TRAIT(src, TRAIT_FLOCK_NODECON, INNATE_TRAIT) // Turfs dont disappear!!!
qdel(GetComponent(/datum/component/flock_protection))
connected_pylons = null
for(var/mob/living/basic/flock/drone/bird in flockrunning_mobs)
if(HAS_TRAIT(bird, TRAIT_FLOCKPHASE))
bird.stop_flockphase(TRUE)
flockrunning_mobs = null
return ..()
/turf/simulated/floor/flock/get_flock_id()
return "Conduit"
/turf/simulated/floor/flock/try_replace_tile(obj/item/stack/tile/T, mob/user, params)
return FALSE
/turf/simulated/floor/flock/burn_tile()
return
/turf/simulated/floor/flock/update_icon_state()
if(broken)
icon_state = "[base_icon_state]-broken"
else
if(is_on)
icon_state = "[base_icon_state]-on"
else
icon_state = base_icon_state
return ..()
/// Turns the tile on or off depending on what state it should be in.
/turf/simulated/floor/flock/proc/update_power()
var/should_be_on = FALSE
if(!broken && (length(connected_pylons) || length(flockrunning_mobs)))
should_be_on = TRUE
if(should_be_on == is_on)
return
is_on = should_be_on
if(is_on)
set_light(4, 0.4, light_color)
else
set_light(0, 0, light_color)
update_appearance(UPDATE_ICON_STATE)
@@ -0,0 +1,47 @@
/turf/simulated/wall/flock
name = "cybernetic wall"
icon = 'icons/goonstation/turf/flock.dmi'
icon_state = "flock-0"
base_icon_state = "flock"
pathing_pass_method = TURF_PATHING_PASS_PROC
light_color = "#19b299"
light_power = 0.5
light_range = 0.6
/turf/simulated/wall/flock/Initialize(mapload)
. = ..()
ADD_TRAIT(src, TRAIT_FLOCK_THING, INNATE_TRAIT)
AddComponent(/datum/component/flock_protection, FALSE, TRUE, FALSE, FALSE)
/turf/simulated/wall/flock/Destroy(force)
REMOVE_TRAIT(src, TRAIT_FLOCK_THING, INNATE_TRAIT) // Turfs are persistent refs
qdel(GetComponent(/datum/component/flock_protection))
return ..()
/turf/simulated/wall/flock/CanPass(atom/movable/mover, border_dir)
. = ..()
if(.)
return
if(!isflockdrone(mover))
return
var/mob/living/basic/flock/drone/bird = mover
if(HAS_TRAIT(bird, TRAIT_FLOCKPHASE))
return TRUE
if(bird.can_flockphase())
return TRUE
/turf/simulated/wall/flock/CanPathfindPass(to_dir, datum/can_pass_info/pass_info)
var/atom/M = locateUID(pass_info.caller_uid)
if(!M)
return ..()
if(!isflockdrone(M))
return ..()
var/mob/living/basic/flock/drone/bird = M
if(!bird.can_flockphase())
return ..()
return TRUE
@@ -0,0 +1,50 @@
/datum/flock_unlockable
// auto populated field
var/name
// auto populated field
var/purchase_cost
var/obj/structure/flock/structure_type
var/unlocked = FALSE
/datum/flock_unlockable/New()
purchase_cost = initial(structure_type.active_bandwidth_cost)
name = "[initial(structure_type.flock_id)] ([purchase_cost])"
/datum/flock_unlockable/proc/refresh_lock_status(datum/flock/flock, total_compute, available_compute)
if(is_unlockable(flock, total_compute, available_compute))
if(!unlocked)
unlock(flock)
else if(unlocked)
lock(flock)
/datum/flock_unlockable/proc/is_unlockable(datum/flock/flock, total_compute, available_compute)
return TRUE
/datum/flock_unlockable/proc/unlock(datum/flock/flock)
unlocked = TRUE
flock_talk(null, "New structure devised: [initial(structure_type.flock_id)]", flock, TRUE)
/datum/flock_unlockable/proc/lock(datum/flock/flock)
unlocked = FALSE
flock_talk(null, "Alert, structure tealprint disabled: [initial(structure_type.flock_id)]", flock, TRUE)
/datum/flock_unlockable/sentinel
structure_type = /obj/structure/flock/sentinel
/datum/flock_unlockable/collector
structure_type = /obj/structure/flock/collector
/datum/flock_unlockable/interceptor
structure_type = /obj/structure/flock/interceptor
/datum/flock_unlockable/turret
structure_type = /obj/structure/flock/gnesis_turret
/datum/flock_unlockable/relay
structure_type = /obj/structure/flock/relay
/datum/flock_unlockable/relay/is_unlockable(datum/flock/flock, total_compute, available_compute)
return flock.total_bandwidth() >= FLOCK_COMPUTE_COST_RELAY && flock.total_turfs() >= FLOCK_TURFS_FOR_RELAY && flock.flock_game_status == NONE
@@ -0,0 +1,87 @@
/datum/reagent/gnesis
name = "coagulated gnesis"
id = "gnesis_tox"
description = "A thick teal fluid of alien origin. It moves in ways that suggest it might be alive in some way."
color = "#4d736d"
taste_description = "oily, with a sweet aftertaste"
metabolization_rate = 0.2
process_flags = ORGANIC | SYNTHETIC
/// The color of the light to give off
var/light_color = "#26ffe6"
/// Reagent -> Gnesis rate per 2 seconds
var/conversion_rate = 0.4
/// Amount of gnesis required to turn the victim into a flockbit
var/flocksplosion_threshold = 200
/// Do not convert these reagents
var/list/do_not_convert = list("charcoal", "calomel", "pen_acid", "gnesis_tox")
/datum/reagent/gnesis/on_new(data)
..()
START_PROCESSING(SSprocessing, src)
/datum/reagent/gnesis/Destroy()
STOP_PROCESSING(SSprocessing, src)
return ..()
/datum/reagent/gnesis/on_mob_life(mob/living/M)
if(volume > 20) // 2.5 bursts of the turret
if(!ismachineperson(M))
M.blood_volume -= conversion_rate
M.reagents.add_reagent("gnesis_tox", conversion_rate)
if(volume > flocksplosion_threshold)
new /mob/living/basic/flock/bit(get_turf(M))
M.gib()
return
if(volume < flocksplosion_threshold / 2)
M.set_light(0, 0, COLOR_WHITE)
if(prob(5))
M.playsound_local(get_turf(M), pick(list('sound/goonstation/flockmind/flockbit_wisp1.ogg',
'sound/goonstation/flockmind/flockbit_wisp2.ogg',
'sound/goonstation/flockmind/flockbit_wisp3.ogg',
'sound/goonstation/flockmind/flockbit_wisp4.ogg',
'sound/goonstation/flockmind/flockbit_wisp5.ogg',
'sound/goonstation/flockmind/flockbit_wisp6.ogg')), 40, FALSE, use_reverb = FALSE)
if(prob(2))
to_chat(M, SPAN_FLOCKSAY("You feel your blood running cold."))
else
if(prob(2) && ishuman(M))
var/mob/living/carbon/human/H = M
H.take_organ_damage(5)
if(prob(10))
M.playsound_local(get_turf(M), pick(list('sound/goonstation/flockmind/flockbit_wisp1.ogg',
'sound/goonstation/flockmind/flockbit_wisp2.ogg',
'sound/goonstation/flockmind/flockbit_wisp3.ogg',
'sound/goonstation/flockmind/flockbit_wisp4.ogg',
'sound/goonstation/flockmind/flockbit_wisp5.ogg',
'sound/goonstation/flockmind/flockbit_wisp6.ogg')), 40, FALSE, use_reverb = FALSE)
if(prob(15))
M.set_light(2, 0.2, light_color)
if(prob(5))
to_chat(M, SPAN_FLOCKSAY("You feel like something wants to burst out of your body!"))
return ..()
/datum/reagent/gnesis/process()
if(..())
for(var/datum/reagent/R in holder.reagent_list)
if(R.id in do_not_convert)
continue
holder.remove_reagent(R.id, conversion_rate)
holder.add_reagent("gnesis_tox", conversion_rate)
/datum/reagent/gnesis/reaction_turf(turf/T, volume, color)
. = ..()
if(isspaceturf(T))
return
if(volume >= 50 && (isfloorturf(T) || iswallturf(T)))
T.visible_message(SPAN_NOTICE("The substance flows out and sinks into [T], forming new shapes."))
flock_convert_turf(T)
return
else if(volume >= 10)
if(prob(50))
new /obj/item/stack/sheet/gnesis(T)
else
new /obj/item/stack/sheet/gnesis_glass(T)
return
T.visible_message(SPAN_NOTICE("The substance flows out, spread too thinly."))