diff --git a/code/__defines/misc.dm b/code/__defines/misc.dm index 4d5a639708f0..8198a65f2394 100644 --- a/code/__defines/misc.dm +++ b/code/__defines/misc.dm @@ -402,3 +402,9 @@ #define MM_ATTACK_RESULT_NONE 0 #define MM_ATTACK_RESULT_DEFLECTED BITFLAG(0) #define MM_ATTACK_RESULT_BLOCKED BITFLAG(1) + +// Effectively a speed modifier for how fast pollen is produced by flowering plants. Pollen per second. +// In theory, one pollen every 5 seconds (at time of writing) +#define POLLEN_PER_SECOND 0.2 +#define POLLEN_PRODUCTION_MULT (POLLEN_PER_SECOND * (SSplants.wait / 10)) +#define MAX_POLLEN_PER_FLOWER 10 diff --git a/code/game/objects/items/devices/chameleonproj.dm b/code/game/objects/items/devices/chameleonproj.dm index fc743829ceef..83f05a1b7de8 100644 --- a/code/game/objects/items/devices/chameleonproj.dm +++ b/code/game/objects/items/devices/chameleonproj.dm @@ -102,7 +102,7 @@ density = FALSE anchored = TRUE is_spawnable_type = FALSE - movement_handlers = list(/datum/movement_handler/delay/chameleon_projector) + movement_handlers = list(/datum/movement_handler/delay/chameleon_projector = list(2.5 SECONDS)) var/obj/item/chameleon/master = null /obj/effect/dummy/chameleon/Initialize(mapload, var/obj/item/chameleon/projector) @@ -152,9 +152,6 @@ if(!my_turf.get_supporting_platform() && !(locate(/obj/structure/lattice) in loc)) disrupted() -/datum/movement_handler/delay/chameleon_projector - delay = 2.5 SECONDS - /datum/movement_handler/delay/chameleon_projector/MayMove(mob/mover, is_external) return host.loc?.has_gravity() ? ..() : MOVEMENT_STOP diff --git a/code/game/objects/structures/flora/plant.dm b/code/game/objects/structures/flora/plant.dm index 9e44f55a2f3a..278a60e8f019 100644 --- a/code/game/objects/structures/flora/plant.dm +++ b/code/game/objects/structures/flora/plant.dm @@ -7,14 +7,20 @@ var/dead = FALSE var/sampled = FALSE var/datum/seed/plant - var/harvestable + var/harvestable = 0 // Note that this is a counter, not a bool. + var/pollen = 0 /obj/structure/flora/plant/large opacity = TRUE density = TRUE -/* Notes for future work moving logic off hydrotrays onto plants themselves: /obj/structure/flora/plant/Process() + if(plant?.produces_pollen <= 0) + return PROCESS_KILL + if(pollen < MAX_POLLEN_PER_FLOWER) + pollen += plant.produces_pollen * POLLEN_PRODUCTION_MULT + +/* Notes for future work moving logic off hydrotrays onto plants themselves: // check our immediate environment // ask our environment for available reagents // process the reagents @@ -61,9 +67,13 @@ var/potency = plant.get_trait(TRAIT_POTENCY) set_light(l_range = max(1, round(potency/10)), l_power = clamp(round(potency/30), 0, 1), l_color = plant.get_trait(TRAIT_BIOLUM_COLOUR)) update_icon() - return ..() + . = ..() + if(plant?.produces_pollen && !is_processing) + START_PROCESSING(SSplants, src) /obj/structure/flora/plant/Destroy() + if(is_processing) + STOP_PROCESSING(SSplants, src) plant = null . = ..() @@ -147,3 +157,24 @@ /obj/structure/flora/plant/random_mushroom/Initialize() plant = pick(get_mushroom_variants()) return ..() + +/obj/structure/flora/plant/random_flower + name = "flower" + color = COLOR_PINK + icon_state = "flower5" + is_spawnable_type = TRUE + +// Only contains roundstart plants, this is meant to be a mapping helper. +/obj/structure/flora/plant/random_flower/proc/get_flower_variants() + var/static/list/flower_variants + if(isnull(flower_variants)) + flower_variants = list() + for(var/plant in SSplants.seeds) + var/datum/seed/seed = SSplants.seeds[plant] + if(!isnull(seed?.name) && seed.produces_pollen) + flower_variants |= seed.name + return flower_variants + +/obj/structure/flora/plant/random_flower/Initialize() + plant = pick(get_flower_variants()) + return ..() diff --git a/code/game/objects/structures/flora/stump.dm b/code/game/objects/structures/flora/stump.dm index 4911cd3a57e7..f13415593f7c 100644 --- a/code/game/objects/structures/flora/stump.dm +++ b/code/game/objects/structures/flora/stump.dm @@ -4,6 +4,7 @@ /obj/structure/flora/stump name = "stump" hitsound = 'sound/effects/hit_wood.ogg' + storage = /datum/storage/dead_tree var/log_type = /obj/item/stack/material/log /obj/structure/flora/stump/get_material_health_modifier() diff --git a/code/game/objects/structures/flora/tree.dm b/code/game/objects/structures/flora/tree.dm index 13dcf346bcc1..80eb4b09a9cf 100644 --- a/code/game/objects/structures/flora/tree.dm +++ b/code/game/objects/structures/flora/tree.dm @@ -114,6 +114,11 @@ var/global/list/christmas_trees = list() icon_state = "tree_1" protects_against_weather = FALSE stump_type = /obj/structure/flora/stump/tree/dead + storage = /datum/storage/dead_tree + +/datum/storage/dead_tree + max_w_class = ITEM_SIZE_NORMAL + max_storage_space = ITEM_SIZE_SMALL * 5 /obj/structure/flora/tree/dead/random/init_appearance() icon_state = "tree_[rand(1, 6)]" diff --git a/code/modules/hydroponics/trays/tray_process.dm b/code/modules/hydroponics/trays/tray_process.dm index a62c81a8d1e2..cd6d30d7a0ad 100644 --- a/code/modules/hydroponics/trays/tray_process.dm +++ b/code/modules/hydroponics/trays/tray_process.dm @@ -65,8 +65,9 @@ mutate((rand(100) < 15) ? 2 : 1) mutation_level = 0 - if(pollen < 10) - pollen += seed?.produces_pollen + if(pollen < MAX_POLLEN_PER_FLOWER) + pollen += seed?.produces_pollen * POLLEN_PRODUCTION_MULT + to_world("\ref[src] has pollen [pollen] ([seed?.produces_pollen] * [POLLEN_PRODUCTION_MULT])") // Maintain tray nutrient and water levels. if(seed.get_trait(TRAIT_REQUIRES_NUTRIENTS) && seed.get_trait(TRAIT_NUTRIENT_CONSUMPTION) > 0 && nutrilevel > 0 && prob(25)) diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index 1e6497c10a04..e965e0fc84de 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -2005,3 +2005,6 @@ default behaviour is: /mob/living/is_cloaked() return has_mob_modifier(/decl/mob_modifier/cloaked) + +/mob/living/proc/is_playing_dead() + return stat || current_posture?.prone || (status_flags & FAKEDEATH) diff --git a/code/modules/mob/mob_automove.dm b/code/modules/mob/mob_automove.dm index f552d4f7c1e6..c74d72c44e97 100644 --- a/code/modules/mob/mob_automove.dm +++ b/code/modules/mob/mob_automove.dm @@ -25,7 +25,6 @@ /mob/failed_automove() ..() stop_automove() - _automove_target = null return FALSE /mob/start_automove(target, movement_type, datum/automove_metadata/metadata) diff --git a/code/modules/mob/skills/skillset.dm b/code/modules/mob/skills/skillset.dm index eef151360637..9da79495a218 100644 --- a/code/modules/mob/skills/skillset.dm +++ b/code/modules/mob/skills/skillset.dm @@ -47,7 +47,8 @@ var/global/list/all_skill_verbs QDEL_NULL(NM) //Clean all nano_modules for simplicity. QDEL_NULL(mob.skillset.NM) QDEL_NULL_LIST(nm_viewing) - QDEL_NULL_LIST(mob.skillset.nm_viewing) + if(mob.skillset) + QDEL_NULL_LIST(mob.skillset.nm_viewing) on_levels_change() //Called when a player is added as an antag and the antag datum processes the skillset. diff --git a/maps/ministation/ministation-1.dmm b/maps/ministation/ministation-1.dmm index 1ff78b8cade6..09ee082eafcb 100644 --- a/maps/ministation/ministation-1.dmm +++ b/maps/ministation/ministation-1.dmm @@ -8744,7 +8744,7 @@ /turf/floor/tiled, /area/ministation/hall/e2) "Oo" = ( -/obj/machinery/beehive, +/obj/structure/apiary/mapped, /turf/floor/fake_grass, /area/ministation/hydro) "Op" = ( diff --git a/mods/content/beekeeping/_beekeeping.dm b/mods/content/beekeeping/_beekeeping.dm index d3b58be746d6..53ca84daf621 100644 --- a/mods/content/beekeeping/_beekeeping.dm +++ b/mods/content/beekeeping/_beekeeping.dm @@ -1,7 +1,13 @@ +#define FRAME_RESERVE_COST 30 +#define SWARM_AGITATION_PER_FRAME 25 +#define FRAME_MATERIAL_COST 20 +#define SWARM_GROWTH_COST 10 +#define FRAME_FILL_MATERIAL_COST 5 +#define HIVE_REPAIR_MATERIAL_COST 5 + /decl/modpack/beekeeping - name = "Beekeeping Content" + name = "Beekeeping and Insects Content" /datum/storage/hopper/industrial/centrifuge/New() ..() can_hold |= /obj/item/hive_frame - diff --git a/mods/content/beekeeping/_beekeeping.dme b/mods/content/beekeeping/_beekeeping.dme index 75778704f368..92ee753943b5 100644 --- a/mods/content/beekeeping/_beekeeping.dme +++ b/mods/content/beekeeping/_beekeeping.dme @@ -3,10 +3,17 @@ // BEGIN_INCLUDE #include "_beekeeping.dm" #include "closets.dm" -#include "hive_frame.dm" #include "items.dm" +#include "materials.dm" #include "recipes.dm" #include "trading.dm" -#include "hives\_hive.dm" +#include "hives\hive_extension.dm" +#include "hives\hive_flora.dm" +#include "hives\hive_frame.dm" +#include "hives\hive_queen.dm" +#include "hives\hive_structure.dm" +#include "hives\hive_swarm.dm" +#include "hives\insect_species\_insects.dm" +#include "hives\insect_species\insects_pollinators.dm" // END_INCLUDE #endif diff --git a/mods/content/beekeeping/closets.dm b/mods/content/beekeeping/closets.dm index c39e70dbb457..daf63a942d5c 100644 --- a/mods/content/beekeeping/closets.dm +++ b/mods/content/beekeeping/closets.dm @@ -1,11 +1,11 @@ /obj/structure/closet/crate/hydroponics/beekeeping name = "beekeeping crate" - desc = "All you need to set up your own beehive." + desc = "All you need to set up your own beehive, except the beehive." /obj/structure/closet/crate/hydroponics/beekeeping/Initialize() . = ..() - new /obj/item/beehive_assembly(src) - new /obj/item/bee_smoker(src) + new /obj/item/stack/material/plank/mapped/wood/ten + new /obj/item/smoker(src) new /obj/item/hive_frame/crafted(src) new /obj/item/hive_frame/crafted(src) new /obj/item/hive_frame/crafted(src) diff --git a/mods/content/beekeeping/hive_frame.dm b/mods/content/beekeeping/hive_frame.dm deleted file mode 100644 index e126ec1a7fa2..000000000000 --- a/mods/content/beekeeping/hive_frame.dm +++ /dev/null @@ -1,55 +0,0 @@ -/obj/item/hive_frame - abstract_type = /obj/item/hive_frame - icon_state = ICON_STATE_WORLD - w_class = ITEM_SIZE_SMALL - material_alteration = MAT_FLAG_ALTERATION_ALL - chem_volume = 20 - var/destroy_on_centrifuge = FALSE - -/obj/item/hive_frame/on_reagent_change() - . = ..() - if(reagents?.total_volume) - SetName("filled [initial(name)] ([reagents.get_primary_reagent_name()])") - else - SetName(initial(name)) - queue_icon_update() - -/obj/item/hive_frame/on_update_icon() - . = ..() - var/mesh_state = "[icon_state]-mesh" - if(check_state_in_icon(mesh_state, icon)) - add_overlay(overlay_image(icon, mesh_state, COLOR_WHITE, RESET_COLOR)) - if(reagents?.total_volume) - var/comb_state = "[icon_state]-comb" - if(check_state_in_icon(comb_state, icon)) - add_overlay(overlay_image(icon, comb_state, reagents.get_color(), RESET_COLOR)) - compile_overlays() - -/obj/item/hive_frame/handle_centrifuge_process(obj/machinery/centrifuge/centrifuge) - if(!(. = ..())) - return - if(reagents.total_volume) - reagents.trans_to_holder(centrifuge.loaded_beaker.reagents, reagents.total_volume) - for(var/obj/item/thing in contents) - thing.dropInto(centrifuge.loc) - if(destroy_on_centrifuge) - for(var/atom/movable/thing in convert_matter_to_lumps()) - thing.dropInto(centrifuge.loc) - -// Crafted frame used in apiaries. -/obj/item/hive_frame/crafted - name = "hive frame" - desc = "A wooden frame for insect hives that the workers will fill with products like honey." - icon = 'mods/content/beekeeping/icons/frame.dmi' - material = /decl/material/solid/organic/wood/oak - material_alteration = MAT_FLAG_ALTERATION_ALL - -// TEMP until beewrite redoes hives. -/obj/item/hive_frame/crafted/filled/Initialize() - . = ..() - new /obj/item/stack/material/bar/wax(src) - update_icon() - -/obj/item/hive_frame/crafted/filled/populate_reagents() - . = ..() - reagents.add_reagent(/decl/material/liquid/nutriment/honey, reagents?.maximum_volume) diff --git a/mods/content/beekeeping/hives/_hive.dm b/mods/content/beekeeping/hives/_hive.dm deleted file mode 100644 index 1ace43bbe1cf..000000000000 --- a/mods/content/beekeeping/hives/_hive.dm +++ /dev/null @@ -1,168 +0,0 @@ -/obj/machinery/beehive - name = "apiary" - icon = 'mods/content/beekeeping/icons/beekeeping.dmi' - icon_state = "beehive-0" - desc = "A wooden box designed specifically to house our buzzling buddies. Far more efficient than traditional hives. Just insert a frame and a queen, close it up, and you're good to go!" - density = TRUE - anchored = TRUE - layer = BELOW_OBJ_LAYER - - var/closed = 0 - var/bee_count = 0 // Percent - var/smoked = 0 // Timer - var/honeycombs = 0 // Percent - var/frames = 0 - var/maxFrames = 5 - -/obj/machinery/beehive/Initialize() - . = ..() - update_icon() - -/obj/machinery/beehive/on_update_icon() - overlays.Cut() - icon_state = "beehive-[closed]" - if(closed) - overlays += "lid" - if(frames) - overlays += "empty[frames]" - if(honeycombs >= 100) - overlays += "full[round(honeycombs / 100)]" - if(!smoked) - switch(bee_count) - if(1 to 20) - overlays += "bees1" - if(21 to 40) - overlays += "bees2" - if(41 to 60) - overlays += "bees3" - if(61 to 80) - overlays += "bees4" - if(81 to 100) - overlays += "bees5" - -/obj/machinery/beehive/get_examine_strings(mob/user, distance, infix, suffix) - . = ..() - if(!closed) - . += "The lid is open." - -/obj/machinery/beehive/attackby(var/obj/item/used_item, var/mob/user) - if(IS_CROWBAR(used_item)) - closed = !closed - user.visible_message("\The [user] [closed ? "closes" : "opens"] \the [src].", "You [closed ? "close" : "open"] \the [src].") - update_icon() - return TRUE - else if(IS_WRENCH(used_item)) - anchored = !anchored - user.visible_message("\The [user] [anchored ? "wrenches" : "unwrenches"] \the [src].", "You [anchored ? "wrench" : "unwrench"] \the [src].") - return TRUE - else if(istype(used_item, /obj/item/bee_smoker)) - if(closed) - to_chat(user, "You need to open \the [src] with a crowbar before smoking the bees.") - return TRUE - user.visible_message("\The [user] smokes the bees in \the [src].", "You smoke the bees in \the [src].") - smoked = 30 - update_icon() - return TRUE - else if(istype(used_item, /obj/item/hive_frame/crafted)) - if(closed) - to_chat(user, "You need to open \the [src] with a crowbar before inserting \the [used_item].") - return TRUE - if(frames >= maxFrames) - to_chat(user, "There is no place for an another frame.") - return TRUE - var/obj/item/hive_frame/crafted/H = used_item - if(H.reagents?.total_volume) - to_chat(user, "\The [used_item] is full with beeswax and honey, empty it in the extractor first.") - return TRUE - ++frames - user.visible_message("\The [user] loads \the [used_item] into \the [src].", "You load \the [used_item] into \the [src].") - update_icon() - qdel(used_item) - return TRUE - else if(istype(used_item, /obj/item/bee_pack)) - var/obj/item/bee_pack/B = used_item - if(B.full && bee_count) - to_chat(user, "\The [src] already has bees inside.") - return TRUE - if(!B.full && bee_count < 90) - to_chat(user, "\The [src] is not ready to split.") - return TRUE - if(!B.full && !smoked) - to_chat(user, "Smoke \the [src] first!") - return TRUE - if(closed) - to_chat(user, "You need to open \the [src] with a crowbar before moving the bees.") - return TRUE - if(B.full) - user.visible_message("\The [user] puts the queen and the bees from \the [used_item] into \the [src].", "You put the queen and the bees from \the [used_item] into \the [src].") - bee_count = 20 - B.empty() - else - user.visible_message("\The [user] puts bees and larvae from \the [src] into \the [used_item].", "You put bees and larvae from \the [src] into \the [used_item].") - bee_count /= 2 - B.fill() - update_icon() - return TRUE - else if(istype(used_item, /obj/item/scanner/plant)) - to_chat(user, "Scan result of \the [src]...") - to_chat(user, "Beehive is [bee_count ? "[round(bee_count)]% full" : "empty"].[bee_count > 90 ? " Colony is ready to split." : ""]") - if(frames) - to_chat(user, "[frames] frames installed, [round(honeycombs / 100)] filled.") - if(honeycombs < frames * 100) - to_chat(user, "Next frame is [round(honeycombs % 100)]% full.") - else - to_chat(user, "No frames installed.") - if(smoked) - to_chat(user, "The hive is smoked.") - return TRUE - else if(IS_SCREWDRIVER(used_item)) - if(bee_count) - to_chat(user, "You can't dismantle \the [src] with these bees inside.") - return TRUE - to_chat(user, "You start dismantling \the [src]...") - playsound(loc, 'sound/items/Screwdriver.ogg', 50, 1) - if(do_after(user, 30, src)) - user.visible_message("\The [user] dismantles \the [src].", "You dismantle \the [src].") - new /obj/item/beehive_assembly(loc) - qdel(src) - return TRUE - return FALSE // this should probably not be a machine, so don't do any component interactions - -/obj/machinery/beehive/physical_attack_hand(var/mob/user) - if(closed) - return FALSE - . = TRUE - if(honeycombs < 100) - to_chat(user, "There are no filled honeycombs.") - return - if(!smoked && bee_count) - to_chat(user, "The bees won't let you take the honeycombs out like this, smoke them first.") - return - user.visible_message("\The [user] starts taking the honeycombs out of \the [src].", "You start taking the honeycombs out of \the [src]...") - while(honeycombs >= 100 && do_after(user, 30, src)) - new /obj/item/hive_frame/crafted/filled(loc) - honeycombs -= 100 - --frames - update_icon() - if(honeycombs < 100) - to_chat(user, "You take all filled honeycombs out.") - -/obj/machinery/beehive/Process() - if(closed && !smoked && bee_count) - pollinate_flowers() - update_icon() - smoked = max(0, smoked - 1) - if(!smoked && bee_count) - bee_count = min(bee_count * 1.005, 100) - update_icon() - -/obj/machinery/beehive/proc/pollinate_flowers() - var/coef = bee_count / 100 - var/trays = 0 - for(var/obj/machinery/portable_atmospherics/hydroponics/H in view(7, src)) - if(H.seed && !H.dead) - H.plant_health += 0.05 * coef - if(H.pollen >= 1) - H.pollen-- - trays++ - honeycombs = min(honeycombs + 0.1 * coef * min(trays, 5), frames * 100) diff --git a/mods/content/beekeeping/hives/hive_extension.dm b/mods/content/beekeeping/hives/hive_extension.dm new file mode 100644 index 000000000000..c315dd59e2ef --- /dev/null +++ b/mods/content/beekeeping/hives/hive_extension.dm @@ -0,0 +1,218 @@ +/datum/extension/insect_hive + base_type = /datum/extension/insect_hive + expected_type = /obj/structure + flags = EXTENSION_FLAG_IMMEDIATE + /// The species of insect that made this hive. + var/decl/insect_species/holding_species + /// References to our current swarm effects gathering for the hive. + var/list/swarms + var/current_health = 100 + var/material = 10 + var/raw_reserves = 0 + /// Tracker for the last world.time that a frame was removed. + var/frame_last_removed = 0 + /// Tracker for time that smoke will wear off. + var/smoked_until = 0 + +/datum/extension/insect_hive/New(datum/holder, _species_decl) + ..() + holding_species = istype(_species_decl, /decl/insect_species) ? _species_decl : GET_DECL(_species_decl) + if(!istype(holding_species)) + CRASH("Insect hive extension instantiated with invalid insect species: '[_species_decl]'.") + START_PROCESSING(SSprocessing, src) + +/datum/extension/insect_hive/Destroy() + STOP_PROCESSING(SSprocessing, src) + if(length(swarms)) + for(var/obj/effect/insect_swarm/swarm as anything in swarms) + swarm.owner = null + swarms = null + var/atom/movable/hive = holder + if(istype(hive) && !QDELETED(hive)) + hive.queue_icon_update() + return ..() + +/datum/extension/insect_hive/Process() + if(world.time < smoked_until) + return + holding_species.process_hive(src) + create_hive_products() + +/datum/extension/insect_hive/proc/handle_item_interaction(mob/user, obj/item/item) + return FALSE + +/datum/extension/insect_hive/proc/drop_nest(atom/drop_loc) + if(!isatom(drop_loc)) + return + // handle some kind of physical hive dropping here + remove_extension(holder, /datum/extension/insect_hive) + if(!QDELETED(src)) + qdel(src) + +/datum/extension/insect_hive/proc/get_nest_condition() + switch(current_health) + if(0, 10) + return "dying" + if(10, 30) + return "struggling" + if(30, 60) + return "sickly" + if(60, 90) + return null + return "thriving" + +/datum/extension/insect_hive/proc/get_nest_name() + return holding_species?.nest_name + +/datum/extension/insect_hive/proc/examined(mob/user, show_detail) + var/nest_descriptor = get_nest_condition() + if(nest_descriptor) + to_chat(user, SPAN_NOTICE("It contains \a [nest_descriptor] [get_nest_name()].")) + else + to_chat(user, SPAN_NOTICE("It contains \a [get_nest_name()].")) + +/datum/extension/insect_hive/proc/frame_removed(obj/item/frame) + frame_last_removed = world.time + if(world.time >= smoked_until && length(swarms) > 0) + if(isatom(holder)) + var/atom/hive = holder + hive.visible_message(SPAN_DANGER("The buzzing from \the [holder] intensifies.")) + for(var/obj/effect/insect_swarm/swarm as anything in swarms) + swarm.swarm_agitation = min(100, swarm.swarm_agitation + SWARM_AGITATION_PER_FRAME) + +/datum/extension/insect_hive/proc/try_hand_harvest(mob/user, obj/item/structure) + if(istype(structure) && !structure.storage) + var/obj/item/hive_frame/frame = locate() in structure + if(frame) + frame.dropInto(get_turf(structure)) + if(istype(user)) + user.put_in_hands(frame) + return TRUE + return FALSE + +/datum/extension/insect_hive/proc/try_tool_harvest(mob/user, obj/item/tool) + return FALSE + +/datum/extension/insect_hive/proc/swarm_destroyed(obj/effect/insect_swarm/swarm) + return + +/datum/extension/insect_hive/proc/swarm_at_hive() + for(var/atom/movable/swarm as anything in swarms) + if(get_turf(swarm) == get_turf(holder)) + return swarm + +/datum/extension/insect_hive/proc/has_material(amt) + return amt <= material + +/datum/extension/insect_hive/proc/consume_material(amt) + if(has_material(amt)) + material = clamp(material-amt, 0, 100) + return TRUE + return FALSE + +/datum/extension/insect_hive/proc/add_material(amt) + material = clamp(material+amt, 0, 100) + return TRUE + +/datum/extension/insect_hive/proc/add_reserves(amt) + raw_reserves = clamp(raw_reserves+amt, 0, 100) + return TRUE + +/datum/extension/insect_hive/proc/has_reserves(amt, raw_reserves_only = TRUE) + if(raw_reserves >= amt) + return TRUE + if(raw_reserves_only) + return FALSE + var/reserve = 0 + for(var/obj/item/frame in holder) + reserve += frame.reagents?.total_volume + if(reserve >= amt) + return TRUE + return FALSE + +/datum/extension/insect_hive/proc/consume_reserves(amt, raw_reserves_only = TRUE) + if(!has_reserves(amt, raw_reserves_only)) + return FALSE + if(raw_reserves >= amt) + raw_reserves -= amt + return TRUE + if(raw_reserves_only) + return FALSE + amt -= raw_reserves + raw_reserves = 0 + for(var/obj/item/frame in holder) + if(!frame.reagents?.total_volume) + continue + var/consume = min(amt, frame.reagents.total_volume) + frame.reagents.remove_any(consume) + amt -= consume + if(amt <= 0) + return TRUE + return FALSE + +/datum/extension/insect_hive/proc/adjust_health(amt) + current_health = clamp(current_health + amt, 0, 100) + if(current_health <= 0) + var/atom/movable/hive = holder + hive.visible_message(SPAN_DANGER("\The [holding_species.nest_name] sags and collapses.")) + remove_extension(holder, base_type) + +/datum/extension/insect_hive/proc/create_hive_products() + + var/atom/movable/hive = holder + if(!istype(hive) || !holding_species) + return TRUE + + if(!swarm_at_hive()) // nobody home to do the work + return TRUE + + // Naturally build up enough material for a new frame (or repairs). + if(!has_material(FRAME_MATERIAL_COST)) + add_material(1) + + // Damaged hives cannot produce combs or honey. + if(current_health < 100) + if(consume_material(HIVE_REPAIR_MATERIAL_COST)) + adjust_health(rand(3,5)) + return TRUE + + if(!has_reserves(FRAME_RESERVE_COST)) + return TRUE + + var/list/holder_contents = hive.get_contained_external_atoms() + for(var/obj/item/hive_frame/frame in holder_contents) + if(!frame.reagents || (frame.reagents.total_volume >= frame.reagents.maximum_volume)) + continue + var/fill_cost = REAGENTS_FREE_SPACE(frame.reagents) + if(has_material(FRAME_FILL_MATERIAL_COST) && has_reserves(fill_cost)) + consume_material(FRAME_FILL_MATERIAL_COST) + consume_reserves(fill_cost) + holding_species.fill_hive_frame(frame) + return TRUE + + var/obj/item/native_frame = holding_species.native_frame_type + var/native_frame_size = initial(native_frame.w_class) + var/space_left = hive.storage.max_storage_space + for(var/obj/item/thing in hive.get_stored_inventory()) + space_left -= thing.w_class + if(space_left < native_frame_size) + return + + // Put a timer check on this to avoid a hive filling up with combs the moment you take 2 frames out. + if(world.time > (frame_last_removed + 2 MINUTES) && space_left >= native_frame_size && consume_material(FRAME_MATERIAL_COST)) + // Frames start empty, and will be filled next run. + // Native 'frames' (combs) are bigger than crafted ones and aren't reusable. + new native_frame(holder, holding_species.produce_material) + hive.storage.update_ui_after_item_insertion() + +/datum/extension/insect_hive/proc/get_total_swarm_intensity() + . = 0 + for(var/obj/effect/insect_swarm/swarm as anything in swarms) + . += swarm.swarm_intensity + +/datum/extension/insect_hive/proc/smoked_by(mob/user, atom/source, smoke_time = 10 SECONDS) + smoked_until = max(smoked_until, world.time + smoke_time) + // this is a little weird due to telekinetic bee smoking but so it goes + for(var/obj/effect/insect_swarm/swarm as anything in swarms) + swarm.was_smoked(max(0, smoked_until-world.time)) + return TRUE diff --git a/mods/content/beekeeping/hives/hive_flora.dm b/mods/content/beekeeping/hives/hive_flora.dm new file mode 100644 index 000000000000..d6bc3a50c34f --- /dev/null +++ b/mods/content/beekeeping/hives/hive_flora.dm @@ -0,0 +1,40 @@ +/obj/structure/flora + /// Percentage chance of trying to spawn an insect hive here, if appropriate. + var/insect_hive_chance = 20 + +/obj/structure/flora/Initialize(ml, _mat, _reinf_mat) + . = ..() + if(insect_hive_chance && length(get_supported_insects())) + return INITIALIZE_HINT_LATELOAD + +/obj/structure/flora/LateInitialize() + ..() + if(prob(insect_hive_chance) && !has_extension(src, /datum/extension/insect_hive)) + var/list/insects = get_supported_insects() + if(length(insects)) + insects = insects.Copy() // don't mutate the static list. + for(var/species_type in insects) + var/decl/insect_species/species = GET_DECL(species_type) + if(!species.can_spawn_in_flora(src)) + insects -= species_type + if(length(insects)) + set_extension(src, /datum/extension/insect_hive, pickweight(insects)) + update_icon() + +// Insect species that can hive in this flora. +/obj/structure/flora/proc/get_supported_insects() + return + +/obj/structure/flora/tree/get_supported_insects() + var/static/list/_insects = list( + /decl/insect_species/honeybees = 10, + ///decl/insect_species/wasps = 1 + ) + return _insects + +/obj/structure/flora/stump/get_supported_insects() + var/static/list/_insects = list( + /decl/insect_species/honeybees = 10, + ///decl/insect_species/wasps = 1 + ) + return _insects diff --git a/mods/content/beekeeping/hives/hive_frame.dm b/mods/content/beekeeping/hives/hive_frame.dm new file mode 100644 index 000000000000..c3a4cdceb5de --- /dev/null +++ b/mods/content/beekeeping/hives/hive_frame.dm @@ -0,0 +1,91 @@ +/obj/item/hive_frame + abstract_type = /obj/item/hive_frame + icon_state = ICON_STATE_WORLD + w_class = ITEM_SIZE_SMALL + material_alteration = MAT_FLAG_ALTERATION_ALL + chem_volume = 20 + var/destroy_on_centrifuge = FALSE + +/obj/item/hive_frame/on_reagent_change() + . = ..() + if(reagents?.total_volume) + SetName("filled [initial(name)] ([reagents.get_primary_reagent_name()])") + else + SetName(initial(name)) + queue_icon_update() + +/obj/item/hive_frame/on_update_icon() + . = ..() + var/mesh_state = "[icon_state]-mesh" + if(check_state_in_icon(mesh_state, icon)) + add_overlay(overlay_image(icon, mesh_state, COLOR_WHITE, RESET_COLOR)) + if(reagents?.total_volume) + var/comb_state = "[icon_state]-comb" + if(check_state_in_icon(comb_state, icon)) + add_overlay(overlay_image(icon, comb_state, reagents.get_color(), RESET_COLOR)) + compile_overlays() + +/obj/item/hive_frame/handle_centrifuge_process(obj/machinery/centrifuge/centrifuge) + if(!(. = ..())) + return + if(reagents.total_volume) + reagents.trans_to_holder(centrifuge.loaded_beaker.reagents, reagents.total_volume) + for(var/obj/item/thing in contents) + thing.dropInto(centrifuge.loc) + if(destroy_on_centrifuge) + for(var/atom/movable/thing in convert_matter_to_lumps()) + thing.dropInto(centrifuge.loc) + +/obj/item/hive_frame/honey/populate_reagents() + . = ..() + var/decl/insect_species/bees = GET_DECL(/decl/insect_species/honeybees) + bees.fill_hive_frame(src) + +/obj/item/hive_frame/forceMove(atom/dest) + var/atom/old_loc = loc + . = ..() + if(. && istype(old_loc)) + check_hive_loc(old_loc) + +/obj/item/hive_frame/Move() + var/atom/old_loc = loc + . = ..() + if(. && istype(old_loc)) + check_hive_loc(old_loc) + +/obj/item/hive_frame/proc/check_hive_loc(atom/check_loc) + var/datum/extension/insect_hive/hive = get_extension(check_loc, /datum/extension/insect_hive) + if(istype(hive) && loc != hive.holder) + hive.frame_removed(src) + +// Crafted frame used in apiaries. +/obj/item/hive_frame/crafted + name = "hive frame" + desc = "A wooden frame for insect hives that the workers will fill with products like honey." + icon = 'mods/content/beekeeping/icons/frame.dmi' + material = /decl/material/solid/organic/wood/oak + +// Raw version of honeycomb for wild hives. +/obj/item/hive_frame/comb + name = "comb" + icon = 'mods/content/beekeeping/icons/comb.dmi' + material = /decl/material/solid/organic/wax + destroy_on_centrifuge = TRUE + material_alteration = MAT_FLAG_ALTERATION_COLOR + is_spawnable_type = FALSE + w_class = ITEM_SIZE_NORMAL // Larger than crafted frames, because you should use crafted frames in your hive. + +/obj/item/hive_frame/comb/Initialize(ml, material_key, decl/insect_species/spawning_hive) + . = ..() + if(istype(spawning_hive)) + SetName(spawning_hive.native_frame_name) + desc = spawning_hive.native_frame_desc + spawning_hive.fill_hive_frame(src) + +// Comb subtype for mapping and debugging. +/obj/item/hive_frame/comb/honey + is_spawnable_type = TRUE + color = COLOR_GOLD + +/obj/item/hive_frame/comb/honey/Initialize(ml, material_key) + return ..(ml, material_key, GET_DECL(/decl/insect_species/honeybees)) diff --git a/mods/content/beekeeping/hives/hive_queen.dm b/mods/content/beekeeping/hives/hive_queen.dm new file mode 100644 index 000000000000..599b487d243e --- /dev/null +++ b/mods/content/beekeeping/hives/hive_queen.dm @@ -0,0 +1,23 @@ +/obj/item/bee_pack + name = "bee pack" + desc = "Contains a queen bee and some worker bees. Everything you'll need to start a hive!" + icon = 'mods/content/beekeeping/icons/bee_pack.dmi' + material = /decl/material/solid/organic/plastic + var/contains_insects = /decl/insect_species/honeybees + +/obj/item/bee_pack/Initialize() + . = ..() + update_icon() + +/obj/item/bee_pack/on_update_icon() + . = ..() + if(contains_insects) + add_overlay("[icon_state]-full") + else + add_overlay("[icon_state]-empty") + +/obj/item/bee_pack/proc/empty() + SetName("empty [initial(name)]") + desc = "A stasis pack for moving bees. It's empty." + contains_insects = null + update_icon() diff --git a/mods/content/beekeeping/hives/hive_structure.dm b/mods/content/beekeeping/hives/hive_structure.dm new file mode 100644 index 000000000000..855f9e42c7b8 --- /dev/null +++ b/mods/content/beekeeping/hives/hive_structure.dm @@ -0,0 +1,86 @@ +/obj/structure/attackby(obj/item/used_item, mob/user) + if((. = ..())) + return + var/datum/extension/insect_hive/hive = get_extension(src, /datum/extension/insect_hive) + if(istype(hive) && hive.handle_item_interaction(user, used_item)) + return TRUE + +/obj/structure/attack_hand(mob/user) + if(has_extension(src, /datum/extension/insect_hive)) + var/datum/extension/insect_hive/hive = get_extension(src, /datum/extension/insect_hive) + if(hive.try_hand_harvest(user, src)) + return TRUE + return ..() + +/obj/structure/attackby(obj/item/used_item, mob/user) + if(has_extension(src, /datum/extension/insect_hive)) + var/datum/extension/insect_hive/hive = get_extension(src, /datum/extension/insect_hive) + if(hive.try_tool_harvest(user, used_item)) + return TRUE + return ..() + +/obj/structure/examined_by(mob/user, distance, infix, suffix) + . = ..() + var/datum/extension/insect_hive/hive = get_extension(src, /datum/extension/insect_hive) + if(istype(hive)) + hive.examined(user, (distance <= 1)) + +/atom/physically_destroyed(var/skip_qdel) + var/datum/extension/insect_hive/hive = get_extension(src, /datum/extension/insect_hive) + hive?.drop_nest(loc) + return ..() + +/obj/structure/dismantle_structure(mob/user) + var/datum/extension/insect_hive/hive = get_extension(src, /datum/extension/insect_hive) + hive?.drop_nest(loc) + return ..() + +// 'proper' nest structure for building and mapping +/obj/structure/apiary + name = "apiary" + desc = "An artificial hive for raising insects, like bees, and harvesting products like honey." + icon = 'mods/content/beekeeping/icons/apiary.dmi' + icon_state = ICON_STATE_WORLD + density = TRUE + anchored = TRUE + storage = /datum/storage/apiary + material_alteration = MAT_FLAG_ALTERATION_ALL + material = /decl/material/solid/organic/wood/oak + color = /decl/material/solid/organic/wood/oak::color + obj_flags = OBJ_FLAG_ANCHORABLE + tool_interaction_flags = (TOOL_INTERACTION_ANCHOR | TOOL_INTERACTION_DECONSTRUCT) + +/obj/structure/apiary/CanPass(atom/movable/mover, turf/target, height=0, air_group=0) + return air_group || height == 0 || !density || (istype(mover) && mover.checkpass(PASS_FLAG_TABLE)) + +/obj/structure/apiary/attackby(obj/item/used_item, mob/user) + + if(istype(used_item, /obj/item/bee_pack)) + var/datum/extension/insect_hive/hive = get_extension(src, /datum/extension/insect_hive) + if(istype(hive)) + to_chat(user, SPAN_WARNING("\The [src] already contains \a [hive.holding_species.nest_name].")) + return TRUE + var/obj/item/bee_pack/pack = used_item + if(!pack.contains_insects) + to_chat(user, SPAN_WARNING("\The [pack] is empty!")) + return TRUE + user.visible_message(SPAN_NOTICE("\The [user] transfers the contents of \the [pack] into \the [src].")) + set_extension(src, /datum/extension/insect_hive, pack.contains_insects) + pack.empty() + return TRUE + + . = ..() + +/datum/storage/apiary + can_hold = list(/obj/item/hive_frame) + max_w_class = ITEM_SIZE_NORMAL + max_storage_space = ITEM_SIZE_SMALL * 5 // Five regular frames. + +/obj/structure/apiary/mapped/Initialize(ml, _mat, _reinf_mat) + . = ..() + for(var/_ = 1 to 5) + new /obj/item/hive_frame/crafted(src) + +/obj/structure/apiary/mapped/bees/Initialize(ml, _mat, _reinf_mat) + set_extension(src, /datum/extension/insect_hive, /decl/insect_species/honeybees) + . = ..() diff --git a/mods/content/beekeeping/hives/hive_swarm.dm b/mods/content/beekeeping/hives/hive_swarm.dm new file mode 100644 index 000000000000..084ef4461d84 --- /dev/null +++ b/mods/content/beekeeping/hives/hive_swarm.dm @@ -0,0 +1,369 @@ +/obj/effect/insect_swarm + anchored = TRUE + is_spawnable_type = FALSE + icon_state = "0" + gender = NEUTER + default_pixel_z = 8 + layer = ABOVE_HUMAN_LAYER + pass_flags = PASS_FLAG_TABLE + movement_handlers = list(/datum/movement_handler/delay/insect_swarm = list(1 SECOND)) + + /// Current movement target for automove (ie. hive, flowers or victim) + VAR_PRIVATE/atom/move_target + /// Reference to our owning hive. + var/datum/extension/insect_hive/owner + /// Reference to our insect archetype. + var/decl/insect_species/insect_type + /// A counter for disturbances to the hive or this swarm, causes them to sting people. + var/swarm_agitation = 0 + /// Percentage value; if it drops to 0, the swarm will be destroyed. + var/swarm_intensity = 1 + /// Cooldown timer for next tick. + VAR_PRIVATE/next_work = 0 + /// Time that smoke will wear off. + var/smoked_until = 0 + +/datum/movement_handler/delay/insect_swarm/DoMove(direction, mob/mover, is_external) + ..() + step(host, direction) + return MOVEMENT_HANDLED + +/obj/effect/insect_swarm/debug/Initialize(mapload) + . = ..(mapload, _insect_type = /decl/insect_species/honeybees) + +/obj/effect/insect_swarm/Initialize(mapload, _insect_type, _hive) + . = ..() + insect_type = istype(_insect_type, /decl/insect_species) ? _insect_type : GET_DECL(_insect_type) + owner = _hive + if(!istype(insect_type)) + PRINT_STACK_TRACE("Insect swarm created with invalid insect type: '[_insect_type]'") + return INITIALIZE_HINT_QDEL + if(!istype(owner)) + PRINT_STACK_TRACE("Insect swarm created with invalid hive: '[owner]'") + return INITIALIZE_HINT_QDEL + update_transform() + update_swarm() + LAZYDISTINCTADD(owner.swarms, src) + START_PROCESSING(SSobj, src) + +/obj/effect/insect_swarm/Destroy() + if(owner) + owner.swarm_destroyed(src) + LAZYREMOVE(owner.swarms, src) + owner = null + stop_automove() + STOP_PROCESSING(SSobj, src) + return ..() + +// Resolves the current swarm amount to a coarser value used for icon state selection. +/obj/effect/insect_swarm/proc/get_swarm_state() + return ceil((swarm_intensity / insect_type.max_swarm_intensity) * insect_type.max_swarm_state) + +/obj/effect/insect_swarm/on_update_icon() + . = ..() + color = insect_type.swarm_color + icon = insect_type.swarm_icon + icon_state = num2text(get_swarm_state()) + if(is_smoked()) + icon_state = "[icon_state]_smoked" + +/obj/effect/insect_swarm/update_transform() + . = ..() + // Some icon variation via transform. + if(prob(75)) + var/matrix/swarm_transform = transform || matrix() + swarm_transform.Turn(pick(90, 180, 270)) + transform = swarm_transform + +/obj/effect/insect_swarm/proc/update_swarm() + update_icon() + if(get_swarm_state() == 1) + SetName(insect_type.name_singular) + desc = insect_type.insect_desc + gender = NEUTER + else + SetName(insect_type.name_plural) + desc = insect_type.swarm_desc + gender = PLURAL + +/obj/effect/insect_swarm/proc/is_agitated() + return QDELETED(owner) || (swarm_agitation > 0 && !is_smoked()) + +/obj/effect/insect_swarm/proc/find_sting_target() + for(var/mob/living/victim in view(7, src)) + if(victim.simulated && !victim.is_playing_dead()) + return victim + +/obj/effect/insect_swarm/proc/merge(obj/effect/insect_swarm/other_swarm) + + // If we can fit into one swarm, just merge us together. + var/total_intensity = swarm_intensity + other_swarm.swarm_intensity + if(total_intensity <= insect_type.max_swarm_intensity) + swarm_intensity = total_intensity + swarm_agitation = max(swarm_agitation, other_swarm.swarm_agitation) + update_swarm() + qdel(other_swarm) + return + + // Otherwise equalize between swarms. + swarm_intensity = floor(total_intensity / 2) + other_swarm.swarm_intensity = total_intensity - swarm_intensity + swarm_agitation = max(swarm_agitation, other_swarm.swarm_agitation) + other_swarm.swarm_agitation = max(swarm_agitation, other_swarm.swarm_agitation) + update_swarm() + other_swarm.update_swarm() + +/obj/effect/insect_swarm/Move() + . = ..() + // Swarms from the same hive in the same loc merge together. + if(. && loc && !QDELETED(src)) + try_consolidate_swarms() + +/obj/effect/insect_swarm/Process() + + // Swarms on a loc should try to merge if possible. + try_consolidate_swarms() + if(QDELETED(src)) + return + + // Swarms with no hive gradually decay to nothing. + if(!owner) + adjust_swarm_intensity(-(rand(1,3))) + if(QDELETED(src)) + return + + if(!move_target || !(move_target in view(5, src))) + stop_automove() + + if(is_smoked()) + return + + // Angry swarms move with purpose. + if(is_agitated()) + swarm_agitation = max(0, swarm_agitation-1) + if(!ismob(move_target)) + var/mob/new_move_target = find_sting_target() + if(istype(new_move_target)) + move_target = new_move_target + if(move_target) + start_automove(move_target) + if(insect_type.sting_amount || insect_type.sting_reagent) + insect_type.try_sting(src, loc) + return + + // Large swarms split if they aren't agitated. + if(swarm_can_split() && isturf(loc)) + var/turf/our_turf = loc + for(var/turf/swarm_turf as anything in RANGE_TURFS(our_turf, 1)) + if(swarm_turf == loc || !swarm_turf.CanPass(src)) + continue + var/new_intensity = round(swarm_intensity/2) + var/obj/effect/insect_swarm/new_swarm = new type(swarm_turf, insect_type, owner) + new_swarm.swarm_intensity = new_intensity + new_swarm.swarm_agitation = swarm_agitation + new_swarm.update_swarm() + swarm_intensity -= new_intensity + update_swarm() + break + + // Sting people, if we are so inclined. + if(insect_type.sting_amount || insect_type.sting_reagent) + insect_type.try_sting(src, loc) + + // Hive behavior is dictated by the hive. + if(owner) + handle_hive_behavior() + return + + // If we're not agitated and don't have a hive, we probably shouldn't be pathing somewhere. + stop_automove() + + // Idle swarms with no hive just wander around. + if(prob(5)) + SelfMove(pick(global.alldirs)) + +/obj/effect/insect_swarm/proc/is_first_swarm_at_hive() + var/atom/movable/hive = owner?.holder + if(!isturf(hive?.loc) || loc != hive.loc) + return FALSE + if(length(owner?.swarms) == 1) + return TRUE + for(var/obj/effect/insect_swarm/swarm in hive.loc) + if(swarm == src) + return TRUE + if(swarm in owner.swarms) + break + return FALSE + +/obj/effect/insect_swarm/failed_automove() + ..() + stop_automove() + return FALSE + +/obj/effect/insect_swarm/get_automove_target(datum/automove_metadata/metadata) + return move_target + +/obj/effect/insect_swarm/stop_automove() + move_target = null + . = ..() + +/obj/effect/insect_swarm/can_do_automated_move(variant_move_delay) + return !is_smoked() + +/obj/effect/insect_swarm/start_automove(target, movement_type, datum/automove_metadata/metadata) + move_target = target + . = ..() + +/obj/effect/insect_swarm/proc/handle_hive_behavior() + + var/atom/movable/hive = owner?.holder + if(!isturf(loc)) + // We've just been created; shunt us out onto the turf. + if(loc == hive) + dropInto(hive.loc) + else + return + + // If we are the first (or only) of our owner swarms in the loc, and we aren't needed, we don't move. Hive needs workers. + if(owner?.has_reserves(FRAME_RESERVE_COST)) + if(is_first_swarm_at_hive()) + stop_automove() + return + if(!hive_has_swarm() && loc != hive.loc) + start_automove(owner.holder) + return + + do_work() + +/obj/effect/insect_swarm/proc/do_work() + stop_automove() + if(prob(25)) + var/step_dir = pick(global.alldirs) + if(get_dist(owner.holder, get_step(loc, step_dir)) <= 2) + SelfMove(step_dir) + +/obj/effect/insect_swarm/proc/hive_has_swarm() + var/atom/movable/hive = owner?.holder + if(!isturf(hive?.loc)) + return FALSE + for(var/obj/item/swarm as anything in owner.swarms) + if(swarm.loc == hive.loc) + return TRUE + return FALSE + +/obj/effect/insect_swarm/proc/adjust_swarm_intensity(amount) + var/old_intensity = swarm_intensity + swarm_intensity = clamp(swarm_intensity + amount, 0, insect_type.max_swarm_intensity) + if(old_intensity != swarm_intensity) + if(swarm_intensity <= 0) + qdel(src) + else + update_swarm() + +/obj/effect/insect_swarm/proc/can_grow() + // higher swarm intensity is only seen during agitated states when they converge on a victim and merge. + return swarm_intensity < insect_type.max_swarm_growth_intensity + +/obj/effect/insect_swarm/proc/can_merge() + return swarm_intensity < (is_agitated() ? insect_type.max_swarm_intensity : insect_type.max_swarm_growth_intensity) + +/obj/effect/insect_swarm/proc/swarm_can_split() + return !is_agitated() && swarm_intensity > insect_type.max_swarm_growth_intensity + +/obj/effect/insect_swarm/proc/try_consolidate_swarms() + if(!can_merge()) + return + for(var/obj/effect/insect_swarm/other_swarm in loc) + if(other_swarm == src || !other_swarm.can_merge() || other_swarm.owner != owner || other_swarm.insect_type != insect_type) + continue + merge(other_swarm) + return + +/obj/effect/insect_swarm/DoMove(direction, mob/mover, is_external) + . = ..() + to_world("swarm tried to move: [.]") + +/obj/effect/insect_swarm/pollinator + var/pollen = 0 + +/obj/effect/insect_swarm/pollinator/do_work() + + // Have a rest/do some work. + if(world.time < next_work) + return + + // Unload pollen into hive. + if(pollen) + if(loc == get_turf(owner.holder)) + owner.add_reserves(pollen) + pollen = 0 + next_work = world.time + 5 SECONDS + stop_automove() + else + start_automove(owner.holder) + return + + // Move to flowers. + if(move_target) + if(get_turf(move_target) == loc || !(move_target in view(src, 7))) + move_target = null + stop_automove() + else + start_automove(move_target) + return + + // Harvest from flowers in our loc. + for(var/obj/machinery/portable_atmospherics/hydroponics/flower in loc) + if(!flower.pollen) + continue + if(flower.seed && !flower.dead) + flower.plant_health += rand(3, 5) + flower.check_plant_health() + pollen += flower.pollen + flower.pollen = 0 + next_work = world.time + 5 SECONDS + stop_automove() + return + + // Same logic for flora. TODO unify these when seeds are rewritten to be less bespoke. + for(var/obj/structure/flora/plant/flower in loc) + if(!flower.pollen) + continue + pollen += flower.pollen + flower.pollen = 0 + next_work = world.time + 5 SECONDS + stop_automove() + return + + // Find a flower. + var/list/all_potential_targets = list() + for(var/thing in view(src, 7)) + if(istype(thing, /obj/machinery/portable_atmospherics/hydroponics)) + var/obj/machinery/portable_atmospherics/hydroponics/flower = thing + if(flower.pollen) + all_potential_targets += flower + else if(istype(thing, /obj/structure/flora/plant)) + var/obj/structure/flora/plant/flower = thing + if(flower.pollen) + all_potential_targets += flower + + var/closest_dist + var/atom/closest_target + for(var/atom/thing as anything in shuffle(all_potential_targets)) + var/next_dist = get_dist(src, thing) + if(isnull(closest_target) || next_dist < closest_dist) + closest_target = thing + closest_dist = next_dist + + if(closest_target) + start_automove(closest_target) + else + start_automove(owner.holder) + +/obj/effect/insect_swarm/proc/was_smoked(smoke_time = 10 SECONDS) + smoked_until = max(smoked_until, world.time + smoke_time) + swarm_agitation = round(swarm_agitation * 0.75) + addtimer(CALLBACK(src, TYPE_PROC_REF(/atom, update_icon), TRUE), smoke_time, (TIMER_UNIQUE|TIMER_OVERRIDE)) + +/obj/effect/insect_swarm/proc/is_smoked() + return world.time < smoked_until \ No newline at end of file diff --git a/mods/content/beekeeping/hives/insect_species/_insects.dm b/mods/content/beekeeping/hives/insect_species/_insects.dm new file mode 100644 index 000000000000..d380f6e2e80d --- /dev/null +++ b/mods/content/beekeeping/hives/insect_species/_insects.dm @@ -0,0 +1,189 @@ +/decl/insect_species + abstract_type = /decl/insect_species + + // Descriptive strings for individual insects and swarms. + var/name_singular + var/name_plural + var/insect_desc + + // Vars for nest description and products. + var/nest_name + var/list/produce_reagents + var/decl/material/produce_material + var/produce_material_amount = 1 + var/native_frame_name = "comb" + var/native_frame_desc = "A wax comb from an insect nest." + var/native_frame_type = /obj/item/hive_frame/comb + + // Visual appearance and behavior of swarms. + var/swarm_desc + var/swarm_color = COLOR_BROWN + var/swarm_icon = 'mods/content/beekeeping/icons/swarm.dmi' + var/swarm_type = /obj/effect/insect_swarm + var/max_swarm_growth_intensity = 50 + var/max_swarm_intensity = 100 + var/max_swarm_state = 6 + + // Venom delivered by swarms whens stinging a victim. + var/sting_reagent + var/sting_amount + +/decl/insect_species/Initialize() + if(produce_material) + produce_material = GET_DECL(produce_material) + return ..() + +/decl/insect_species/validate() + . = ..() + + if(!name_singular) + . += "no singular name set" + if(!name_plural) + . += "no plural name set" + if(!nest_name) + . += "no nest name set" + if(!insect_desc) + . += "no insect desc set" + + if(swarm_type) + if(!ispath(swarm_type, /obj/effect/insect_swarm)) + . += "invalid swarm path (must be /obj/effect/insect_swarm or subtype): '[swarm_type]'" + if(!swarm_desc) + . += "no swarm description set" + + if(produce_reagents) + if(!length(produce_reagents) || !islist(produce_reagents)) + . += "empty or non-list produce_reagents" + else + var/total = 0 + for(var/reagent in produce_reagents) + if(!ispath(reagent, /decl/material)) + . += "non-material produce_reagents entry '[reagent]'" + continue + var/amt = produce_reagents[reagent] + if(!isnum(amt) || amt <= 0) + . += "non-numerical or 0 produce_reagents value: '[reagent]', '[amt]'" + total += amt + if(total != 1) + . += "produce_reagents weighting does not sum to 1: '[total]'" + + if(produce_material) + if(!isnum(produce_material_amount) || produce_material_amount <= 0) + . += "non-numeric or zero produce amount: '[produce_material_amount]'" + if(!istype(produce_material, /decl/material)) + . += "non-material product material type: '[produce_material]'" + + if(!swarm_icon) + . += "null swarm icon" + else + for(var/i = 0 to max_swarm_state) + var/check_state = num2text(i) + if(!check_state_in_icon(check_state, swarm_icon)) + . += "missing active icon_state '[check_state]'" + check_state = "[check_state]_smoked" + if(!check_state_in_icon(check_state, swarm_icon)) + . += "missing smoked icon_state '[check_state]'" + +/decl/insect_species/proc/fill_hive_frame(obj/item/frame) + + if(!istype(frame) || QDELETED(frame)) + return FALSE + + var/frame_space = REAGENTS_FREE_SPACE(frame.reagents) + if(frame_space <= 0) + return FALSE + + if(frame.reagents?.maximum_volume && length(produce_reagents)) + var/reagent_split = max(1, floor(min(REAGENTS_FREE_SPACE(frame.reagents), 20) / length(produce_reagents))) + for(var/reagent in produce_reagents) + frame.reagents.add_reagent(reagent, max(1, (reagent_split * produce_reagents[reagent])), defer_update = TRUE) + frame.reagents.handle_update() + if(produce_material && (frame.material != produce_material) && !(locate(/obj/item/stack/material/lump) in frame)) + for(var/atom/movable/thing in produce_material.create_object(frame, produce_material_amount, /obj/item/stack/material/lump)) + thing.forceMove(frame) + return TRUE + +/decl/insect_species/proc/try_sting(obj/effect/insect_swarm/swarm, atom/loc) + if(!istype(swarm) || QDELETED(swarm) || !istype(loc)) + return FALSE + // If we're agitated, always sting. Otherwise, % chance equal to a quarter of our overall swarm intensity. + if(!swarm.is_agitated() && !prob(max(1, round(swarm.swarm_intensity/4)))) + return FALSE + var/base_sting_chance = (sting_amount * clamp(round(swarm.swarm_intensity/10), 1, 10)) + var/sting_mult = swarm.is_agitated() ? max(base_sting_chance, 65) : base_sting_chance + for(var/mob/living/victim in loc) + if(!victim.simulated || victim.stat || victim.current_posture?.prone) + continue + var/datum/reagents/injected_reagents = victim.get_injected_reagents() + var/obj/item/organ/external/affecting = victim.get_organ(pick(global.all_limb_tags)) + if(!affecting || BP_IS_PROSTHETIC(affecting) || BP_IS_CRYSTAL(affecting)) + continue + if(injected_reagents && victim.can_inject(victim, affecting.organ_tag)) + to_chat(victim, SPAN_DANGER("\A [swarm] stings you [sting_mult <= sting_amount * 2 ? "" : "multiple times"] on your [affecting.name]!")) + injected_reagents.add_reagent(sting_reagent, sting_mult) + affecting.add_pain(sting_mult) + . = TRUE + +/decl/insect_species/proc/can_spawn_in_flora(var/obj/structure/flora) + + // Territory range. + for(var/obj/structure/flora/plant in view(flora, 7)) + if(has_extension(plant, /datum/extension/insect_hive)) + return FALSE + + // Food source. + for(var/obj/machinery/portable_atmospherics/hydroponics/flower in view(flora, 7)) + if(flower.seed?.produces_pollen) + return TRUE + + for(var/obj/structure/flora/plant/flower in view(flora, 7)) + if(flower.plant?.produces_pollen) + return TRUE + + + return FALSE + +/decl/insect_species/proc/process_hive(datum/extension/insect_hive/hive_metadata) + + // Sanity check. + var/atom/movable/hive = hive_metadata.holder + if(!istype(hive) || !swarm_type || !istype(hive_metadata)) + return + + // Make sure we always have at least one swarm. + if(!length(hive_metadata.swarms)) + new swarm_type(hive, src, hive_metadata) + + // Reduce swarms if we have too many. + var/swarm_intensity = hive_metadata.get_total_swarm_intensity() + if(swarm_intensity > max_swarm_intensity && length(hive_metadata.swarms)) + var/obj/effect/insect_swarm/swarm = hive_metadata.swarms[1] + swarm.adjust_swarm_intensity(-(swarm_intensity-max_swarm_intensity)) + return + + // Try to grow an existing swarm until we're at our max. + if(hive_metadata.has_reserves(SWARM_GROWTH_COST) && length(hive_metadata.swarms)) + for(var/obj/effect/insect_swarm/swarm as anything in hive_metadata.swarms) + if(swarm.can_grow() && hive_metadata.consume_reserves(SWARM_GROWTH_COST)) + swarm.adjust_swarm_intensity(min(max_swarm_growth_intensity-swarm_intensity, rand(3,5))) + return + + // If we have sufficient filled combs, create a new swarm. Otherwise, expand a swarm. + if(hive.loc && hive_metadata.has_reserves(SWARM_GROWTH_COST)) + + var/obj/effect/insect_swarm/swarm + for(var/obj/effect/insect_swarm/check_swarm as anything in hive_metadata.swarms) + if(check_swarm.loc == hive.loc && check_swarm.can_grow()) + swarm = check_swarm + break + + if(!swarm) + var/comb_count = 0 + for(var/obj/item/hive_frame/frame in hive) + if(frame.reagents && frame.reagents.total_volume >= frame.reagents.maximum_volume) + comb_count++ + if(length(hive_metadata.swarms) < comb_count) + swarm = new swarm_type(hive.loc, src, hive_metadata) + + if(!QDELETED(swarm) && istype(swarm) && hive_metadata.consume_reserves(SWARM_GROWTH_COST)) + swarm.adjust_swarm_intensity(min((max_swarm_growth_intensity-swarm_intensity), rand(3,5))) diff --git a/mods/content/beekeeping/hives/insect_species/insects_pollinators.dm b/mods/content/beekeeping/hives/insect_species/insects_pollinators.dm new file mode 100644 index 000000000000..83aeb86f8462 --- /dev/null +++ b/mods/content/beekeeping/hives/insect_species/insects_pollinators.dm @@ -0,0 +1,28 @@ +/decl/insect_species/honeybees + name_singular = "honeybee" + name_plural = "honeybees" + nest_name = "beehive" + native_frame_name = "honeycomb" + native_frame_desc = "A lattice of hexagonal wax cells usually filled with honey." + native_frame_type = /obj/item/hive_frame/comb + swarm_desc = "A swarm of buzzing honeybees." + insect_desc = "A single buzzing honeybee." + swarm_color = COLOR_GOLD + swarm_type = /obj/effect/insect_swarm/pollinator + sting_reagent = /decl/material/liquid/bee_venom + sting_amount = 1 + produce_reagents = list(/decl/material/liquid/nutriment/honey = 1) + produce_material = /decl/material/solid/organic/wax + +/* +/decl/insect_species/wasps + name_singular = "wasp" + name_plural = "wasps" + nest_name = "wasp hive" + swarm_desc = "A swarm of humming wasps." + insect_desc = "A solitary wasp." + sting_reagent = /decl/material/liquid/cyanide + sting_amount = 5 + swarm_color = COLOR_BRONZE + swarm_type = /obj/effect/insect_swarm/pollinator // tarantula hunter... +*/ \ No newline at end of file diff --git a/mods/content/beekeeping/icons/apiary.dmi b/mods/content/beekeeping/icons/apiary.dmi new file mode 100644 index 000000000000..7ad80a4408f3 Binary files /dev/null and b/mods/content/beekeeping/icons/apiary.dmi differ diff --git a/mods/content/beekeeping/icons/apiary_bees_etc.dmi b/mods/content/beekeeping/icons/apiary_bees_etc.dmi deleted file mode 100644 index 130120bb08fc..000000000000 Binary files a/mods/content/beekeeping/icons/apiary_bees_etc.dmi and /dev/null differ diff --git a/mods/content/beekeeping/icons/bee_pack.dmi b/mods/content/beekeeping/icons/bee_pack.dmi new file mode 100644 index 000000000000..fe68dad8b4b7 Binary files /dev/null and b/mods/content/beekeeping/icons/bee_pack.dmi differ diff --git a/mods/content/beekeeping/icons/beehive.dmi b/mods/content/beekeeping/icons/beehive.dmi new file mode 100644 index 000000000000..9b58ec19c1b7 Binary files /dev/null and b/mods/content/beekeeping/icons/beehive.dmi differ diff --git a/mods/content/beekeeping/icons/beekeeping.dmi b/mods/content/beekeeping/icons/beekeeping.dmi deleted file mode 100644 index 569122d4a58a..000000000000 Binary files a/mods/content/beekeeping/icons/beekeeping.dmi and /dev/null differ diff --git a/mods/content/beekeeping/icons/comb.dmi b/mods/content/beekeeping/icons/comb.dmi new file mode 100644 index 000000000000..edb611c93ae4 Binary files /dev/null and b/mods/content/beekeeping/icons/comb.dmi differ diff --git a/mods/content/beekeeping/icons/smoker.dmi b/mods/content/beekeeping/icons/smoker.dmi index 5109ca468e5e..e6b14a8576cd 100644 Binary files a/mods/content/beekeeping/icons/smoker.dmi and b/mods/content/beekeeping/icons/smoker.dmi differ diff --git a/mods/content/beekeeping/icons/swarm.dmi b/mods/content/beekeeping/icons/swarm.dmi new file mode 100644 index 000000000000..e55d936c1f15 Binary files /dev/null and b/mods/content/beekeeping/icons/swarm.dmi differ diff --git a/mods/content/beekeeping/items.dm b/mods/content/beekeeping/items.dm index 2c15fa75cd04..f520e5085eab 100644 --- a/mods/content/beekeeping/items.dm +++ b/mods/content/beekeeping/items.dm @@ -1,47 +1,34 @@ -/obj/item/beehive_assembly - name = "beehive assembly" - desc = "Contains everything you need to build a beehive." - icon = 'mods/content/beekeeping/icons/apiary_bees_etc.dmi' - icon_state = "apiary" - material = /decl/material/solid/organic/wood/oak - -/obj/item/beehive_assembly/attack_self(var/mob/user) - to_chat(user, "You start assembling \the [src]...") - if(do_after(user, 30, src)) - user.visible_message("\The [user] constructs a beehive.", "You construct a beehive.") - new /obj/machinery/beehive(get_turf(user)) - qdel(src) - -/obj/item/bee_smoker - name = "bee smoker" - desc = "A device used to calm down bees before harvesting honey." +/obj/item/smoker + name = "smoker" + desc = "A device used to calm insects down before harvesting from a hive." icon = 'mods/content/beekeeping/icons/smoker.dmi' icon_state = ICON_STATE_WORLD w_class = ITEM_SIZE_SMALL material = /decl/material/solid/metal/steel -/obj/item/bee_pack - name = "bee pack" - desc = "Contains a queen bee and some worker bees. Everything you'll need to start a hive!" - icon = 'mods/content/beekeeping/icons/beekeeping.dmi' - icon_state = "beepack" - material = /decl/material/solid/organic/plastic - var/full = 1 +// TODO: consume reagents or charges? Unnecessary complexity? +/obj/item/smoker/resolve_attackby(atom/A, mob/user, click_params) + + if(!user.check_dexterity(get_required_attack_dexterity(user, A))) + return TRUE + + var/smoked = FALSE + if(has_extension(A, /datum/extension/insect_hive)) + var/datum/extension/insect_hive/hive = get_extension(A, /datum/extension/insect_hive) + if(hive.smoked_by(user, A)) + smoked = TRUE -/obj/item/bee_pack/Initialize() - . = ..() - overlays += "beepack-full" + if(!smoked && isturf(A)) + for(var/obj/effect/insect_swarm/swarm in A) + swarm.was_smoked() + smoked = TRUE -/obj/item/bee_pack/proc/empty() - full = 0 - name = "empty bee pack" - desc = "A stasis pack for moving bees. It's empty." - overlays.Cut() - overlays += "beepack-empty" + if(smoked) + var/turf/smoked_turf = get_turf(A) + if(smoked_turf) + playsound(smoked_turf, 'sound/effects/refill.ogg', 25, 1) + user.visible_message(SPAN_NOTICE("\The [user] douses \the [A] in smoke from \the [src].")) + new /obj/effect/effect/smoke(smoked_turf, 2 SECONDS) + return TRUE -/obj/item/bee_pack/proc/fill() - full = initial(full) - SetName(initial(name)) - desc = initial(desc) - overlays.Cut() - overlays += "beepack-full" + return ..() diff --git a/mods/content/beekeeping/materials.dm b/mods/content/beekeeping/materials.dm new file mode 100644 index 000000000000..496d5f9b394c --- /dev/null +++ b/mods/content/beekeeping/materials.dm @@ -0,0 +1,20 @@ +/decl/material/liquid/bee_venom + name = "bee venom" + uid = "liquid_venom_bee" + lore_text = "An irritant used by bees to drive off predators." + taste_description = "noxious bitterness" + color = "#d7d891" + heating_products = list( + /decl/material/liquid/denatured_toxin = 1 + ) + heating_point = 100 CELSIUS + heating_message = "becomes clear." + taste_mult = 1.2 + metabolism = REM * 0.25 + exoplanet_rarity_plant = MAT_RARITY_UNCOMMON + exoplanet_rarity_gas = MAT_RARITY_EXOTIC + +/decl/material/liquid/bee_venom/affect_blood(mob/living/M, removed, datum/reagents/holder) + . = ..() + if(istype(M)) + M.adjustHalLoss(max(1, ceil(removed * 10))) diff --git a/mods/content/beekeeping/recipes.dm b/mods/content/beekeeping/recipes.dm index 8494617d3477..3d8515113826 100644 --- a/mods/content/beekeeping/recipes.dm +++ b/mods/content/beekeeping/recipes.dm @@ -1,6 +1,5 @@ -/decl/stack_recipe/planks/beehive_assembly - result_type = /obj/item/beehive_assembly - category = "furniture" +/decl/stack_recipe/planks/furniture/apiary + result_type = /obj/structure/apiary /decl/stack_recipe/planks/beehive_frame - result_type = /obj/item/hive_frame/crafted + result_type = /obj/item/hive_frame/crafted diff --git a/mods/content/beekeeping/trading.dm b/mods/content/beekeeping/trading.dm index 45009265f1bf..c63d01f412c2 100644 --- a/mods/content/beekeeping/trading.dm +++ b/mods/content/beekeeping/trading.dm @@ -1,15 +1,15 @@ /datum/trader/trading_beacon/manufacturing/New() - LAZYSET(possible_trading_items, /obj/item/bee_pack, TRADER_THIS_TYPE) - LAZYSET(possible_trading_items, /obj/item/bee_smoker, TRADER_THIS_TYPE) - LAZYSET(possible_trading_items, /obj/item/beehive_assembly, TRADER_THIS_TYPE) - LAZYSET(possible_trading_items, /obj/item/hive_frame/crafted, TRADER_THIS_TYPE) + LAZYSET(possible_trading_items, /obj/item/bee_pack, TRADER_THIS_TYPE) + LAZYSET(possible_trading_items, /obj/item/smoker, TRADER_THIS_TYPE) + LAZYSET(possible_trading_items, /obj/item/hive_frame/crafted, TRADER_THIS_TYPE) + LAZYSET(possible_trading_items, /obj/item/stack/material/plank/mapped/wood/ten, TRADER_THIS_TYPE) ..() /decl/hierarchy/supply_pack/hydroponics/bee_keeper name = "Equipment - Beekeeping" contains = list( - /obj/item/beehive_assembly, - /obj/item/bee_smoker, + /obj/item/stack/material/plank/mapped/wood/ten, + /obj/item/smoker, /obj/item/hive_frame/crafted = 5, /obj/item/bee_pack )