diff --git a/_maps/map_files/debug/roguetest.dmm b/_maps/map_files/debug/roguetest.dmm index 814d0b18a0c..809a499a949 100644 --- a/_maps/map_files/debug/roguetest.dmm +++ b/_maps/map_files/debug/roguetest.dmm @@ -1116,6 +1116,10 @@ }, /turf/open/floor/cobblerock, /area/outdoors/mountains/decap) +"eg" = ( +/obj/effect/landmark/quest_spawner/easy, +/turf/open/floor/grass/yel, +/area/outdoors) "ek" = ( /obj/machinery/light/fueled/torchholder/hotspring/standing{ pixel_x = 15 @@ -2405,6 +2409,7 @@ dir = 1; pixel_y = -5 }, +/obj/structure/fake_machine/contractledger, /turf/open/floor/wood, /area/indoors/town/keep) "Be" = ( @@ -3042,6 +3047,11 @@ /obj/effect/landmark/start/consort, /turf/open/floor/grass/yel, /area/outdoors) +"Pn" = ( +/obj/structure/flora/grass, +/obj/effect/landmark/quest_spawner/medium, +/turf/open/floor/grass/yel, +/area/outdoors) "Pq" = ( /obj/machinery/light/fueled/smelter/great, /turf/open/floor/cobble, @@ -3527,6 +3537,10 @@ /obj/structure/flora/grass, /turf/open/floor/grass/yel, /area/outdoors) +"YO" = ( +/obj/effect/landmark/quest_spawner/hard, +/turf/open/floor/grass/yel, +/area/outdoors) "YR" = ( /obj/structure/hotspring/border/twelve, /turf/open/floor/cobblerock, @@ -16142,7 +16156,7 @@ QN Wr QN aq -QN +eg QN ky ew @@ -22302,7 +22316,7 @@ tA tA tA ai -QN +YO ai tA tA @@ -76017,7 +76031,7 @@ QN qc QN ky -qc +Pn QN ky jC diff --git a/_maps/map_files/vanderlin/vanderlin.dmm b/_maps/map_files/vanderlin/vanderlin.dmm index 0d4b9c7fdc2..07e5874e6fb 100644 --- a/_maps/map_files/vanderlin/vanderlin.dmm +++ b/_maps/map_files/vanderlin/vanderlin.dmm @@ -45346,6 +45346,10 @@ /mob/living/carbon/human/species/rousman/npc, /turf/open/floor/cobble, /area/indoors/dungeon) +"vJt" = ( +/obj/structure/fake_machine/contractledger, +/turf/open/floor/ruinedwood/darker, +/area/indoors/town/tavern) "vJB" = ( /obj/structure/table/wood/plain_alt, /obj/item/reagent_containers/glass/bowl{ @@ -114547,7 +114551,7 @@ wrm wrm wrm bqc -cfr +vJt cfr kse itp diff --git a/_maps/map_files/vanderlin/vanderlin_forest.dmm b/_maps/map_files/vanderlin/vanderlin_forest.dmm index 9a2ec849428..5d7d13154b2 100644 --- a/_maps/map_files/vanderlin/vanderlin_forest.dmm +++ b/_maps/map_files/vanderlin/vanderlin_forest.dmm @@ -204,6 +204,10 @@ /obj/item/reagent_containers/glass/cup/wooden, /turf/open/floor/tile/kitchen, /area/indoors/wilderness/tavern) +"dq" = ( +/obj/effect/landmark/quest_spawner/hard, +/turf/open/floor/grass, +/area/outdoors/wilderness) "dr" = ( /obj/structure/door/stone, /obj/structure/trap/shock, @@ -306,6 +310,10 @@ /obj/structure/bed/hay, /turf/open/floor/cobble, /area/indoors/wilderness/garrison) +"eC" = ( +/obj/effect/landmark/quest_spawner/medium, +/turf/open/floor/grass, +/area/outdoors/wilderness) "eD" = ( /obj/effect/decal/cleanable/sigil/S, /turf/open/floor/grass, @@ -1043,6 +1051,10 @@ /obj/machinery/light/fueled/wallfire/candle/weak, /turf/open/floor/dirt, /area/outdoors/wilderness) +"lZ" = ( +/obj/effect/landmark/quest_spawner/hard, +/turf/open/floor/dirt/road, +/area/outdoors/wilderness) "md" = ( /mob/living/simple_animal/hostile/retaliate/elemental/behemoth{ color = "#FFC000" @@ -1057,6 +1069,10 @@ "mk" = ( /turf/open/floor/tile/masonic/spiral, /area/indoors/dungeon) +"ml" = ( +/obj/effect/landmark/quest_spawner/easy, +/turf/open/floor/dirt, +/area/outdoors/wilderness) "mn" = ( /turf/closed/basic, /area/outdoors/wilderness) @@ -1543,6 +1559,10 @@ "sd" = ( /turf/closed/wall/mineral/wooddark, /area/outdoors/wilderness) +"sh" = ( +/obj/effect/landmark/quest_spawner/medium, +/turf/open/floor/dirt, +/area/outdoors/wilderness) "sk" = ( /obj/structure/fluff/walldeco/vinez, /turf/closed/wall/mineral/decowood, @@ -2043,6 +2063,10 @@ "yg" = ( /turf/closed/wall/mineral/stone/moss, /area/outdoors/wilderness) +"yj" = ( +/obj/effect/landmark/quest_spawner/easy, +/turf/open/floor/grass, +/area/outdoors/wilderness) "yl" = ( /obj/structure/fluff/walldeco/painting/skull, /turf/closed/wall/mineral/craftstone, @@ -4077,6 +4101,10 @@ "Uj" = ( /turf/closed/mineral/random/med, /area/indoors/cave) +"Un" = ( +/obj/effect/landmark/quest_spawner/hard, +/turf/open/floor/dirt, +/area/outdoors/wilderness) "Uo" = ( /obj/structure/door/stone, /obj/effect/mapping_helpers/access/locker, @@ -4199,6 +4227,10 @@ /obj/structure/closet/crate/drawer/inn, /turf/open/floor/twig, /area/indoors/wilderness/tavern) +"VC" = ( +/obj/effect/landmark/quest_spawner/medium, +/turf/open/floor/dirt/road, +/area/outdoors/wilderness) "VJ" = ( /obj/structure/window/openclose{ dir = 4 @@ -8084,7 +8116,7 @@ ZV Nf ZV ZV -ZV +dq tk ZV tk @@ -8869,7 +8901,7 @@ Uj xa ZV gr -gr +Un ZV gr ZV @@ -8937,7 +8969,7 @@ tk gr gr ZV -gr +Un gr ZV ZV @@ -9364,7 +9396,7 @@ Nf Nf ZV gr -ZV +dq gr ZV Nf @@ -9588,7 +9620,7 @@ tk tk ZV ZV -ZV +eC tk ZV gr @@ -9887,7 +9919,7 @@ ZV gr ZV ZV -ZV +eC gr ZV ZV @@ -10282,7 +10314,7 @@ gr ZV gr ZV -gr +sh ZV ZV ZV @@ -10515,7 +10547,7 @@ ZV Nf gr ZV -gr +sh ZV gr IK @@ -11345,7 +11377,7 @@ ZV gr ZV ZV -ZV +eC ZV ZV Nf @@ -12774,7 +12806,7 @@ ZV tk ZV ZV -ZV +yj ZV ZV Nf @@ -13028,7 +13060,7 @@ gr Nf Nf ZV -gr +Un gr gr ZV @@ -14322,7 +14354,7 @@ ZV gr Nf Nf -ZV +yj ZV ZV tk @@ -14495,7 +14527,7 @@ gr ZV ZV ZV -gr +sh gr ZV tk @@ -14984,7 +15016,7 @@ gr ZV Nf ZV -ZV +yj ZV ZV ZV @@ -15705,7 +15737,7 @@ ZV ZV Nf ZV -gr +Un tk ZV RO @@ -15754,7 +15786,7 @@ gr gr ZV gr -ZV +yj ZV ZV tk @@ -15979,7 +16011,7 @@ ZV Nf Nf ZV -ZV +yj gr gr tk @@ -15988,7 +16020,7 @@ gr Nf tk ZV -ZV +eC IK gr gr @@ -16224,7 +16256,7 @@ ZV ZV gr ZV -ZV +yj ZV gr gr @@ -16884,7 +16916,7 @@ gr TW gr gr -ZV +yj ZV Nf tk @@ -17110,7 +17142,7 @@ ZV Fw ZV Nf -ZV +yj iQ Iz iQ @@ -17966,7 +17998,7 @@ ZV ZV ZV ZV -ZV +eC gr gr tk @@ -18330,7 +18362,7 @@ Nf Nf gr ZV -gr +ml ZV RO gr @@ -18479,7 +18511,7 @@ ZV ZV gr gr -ZV +yj tk ZV tk @@ -18582,7 +18614,7 @@ Nf ZV ZV gr -gr +ml ZV gr tk @@ -19297,7 +19329,7 @@ Iz iQ Iz gr -gr +ml gr Nf gr @@ -19454,7 +19486,7 @@ gr gr zd zd -ZV +yj gr ZV ZV @@ -19572,7 +19604,7 @@ gr ZV Nf gr -gr +Un gr gr Nf @@ -19835,7 +19867,7 @@ gr ZV ZV ZV -ZV +eC gr gr ZV @@ -20009,7 +20041,7 @@ gr gr ZV tk -ZV +yj gr Nf tk @@ -20279,7 +20311,7 @@ gr ZV ZV ZV -ZV +yj ZV ZV zd @@ -20672,7 +20704,7 @@ ZV yV ZV ZV -ZV +yj zd gr gr @@ -21667,7 +21699,7 @@ Nf Nf ZV ZV -ZV +eC gr ZV ZV @@ -21763,7 +21795,7 @@ ZV ZV Nf gr -gr +sh ZV ZV Nf @@ -22698,7 +22730,7 @@ gr Nf tk ZV -gr +sh gr ZV gr @@ -22740,7 +22772,7 @@ iQ HC HC ZV -gr +ml ZV Fw Fw @@ -24149,7 +24181,7 @@ Fw gr ZV ZV -gr +ml gr Nf tk @@ -27014,7 +27046,7 @@ Nf ZV ZV ZV -gr +Un tk gr gr @@ -27043,7 +27075,7 @@ ZV gr gr gr -gr +sh gr gr ZV @@ -27588,7 +27620,7 @@ tk ZV ZV gr -gr +ml gr gr HC @@ -28565,7 +28597,7 @@ Nf Nf Nf ZV -gr +sh gr gr gr @@ -29041,7 +29073,7 @@ gr ZV ZV ZV -tk +sh gr ZV gr @@ -29464,7 +29496,7 @@ Nf gr Nf Iz -ZV +dq gr gr ZV @@ -29517,7 +29549,7 @@ ZV ZV Iz Iz -Iz +VC ZV gr ZV @@ -29689,7 +29721,7 @@ tk ZV ZV Nf -ZV +eC ZV gr Nf @@ -30641,7 +30673,7 @@ iQ Nf ZV ZV -gr +ml ZV gr gr @@ -32322,7 +32354,7 @@ Nf ZV ZV gr -gr +Un ZV tk ZV @@ -32460,7 +32492,7 @@ gr gr gr ZV -gr +ml tk tk tk @@ -32847,7 +32879,7 @@ gr ZV Nf gr -ZV +yj gr Nf Nf @@ -33313,7 +33345,7 @@ hx tk ZV Nf -ZV +dq gr gr Nf @@ -33554,7 +33586,7 @@ ZV ZV ZV ZV -gr +Un ZV Nf Nf @@ -35437,7 +35469,7 @@ gr gr ZV gr -ZV +eC gr Nf Nf @@ -35473,7 +35505,7 @@ Iz Iz ZV gr -ZV +yj gr gr gr @@ -36509,7 +36541,7 @@ Jo gr ZV ZV -ZV +eC gr ZV gr @@ -38188,7 +38220,7 @@ Nf gr gr ZV -ZV +dq yC Iz ZV @@ -39909,7 +39941,7 @@ ZV gr tk gr -ZV +yj ZV ZV Nf @@ -41063,7 +41095,7 @@ ZV gr ZV ZV -ZV +dq ZV tk yC @@ -41236,7 +41268,7 @@ gr gr Iz Iz -Iz +lZ Iz gr Nf @@ -41277,7 +41309,7 @@ yC ZV ZV gr -Iz +VC gr gr Nf @@ -41498,7 +41530,7 @@ Iz ZV ZV ZV -ZV +yj ZV ZV ZV @@ -43446,7 +43478,7 @@ ZV gr ZV ZV -ZV +dq gr Nf Nf diff --git a/code/__DEFINES/ai/_ai.dm b/code/__DEFINES/ai/_ai.dm index 6df8f23a3ed..f237c601e14 100644 --- a/code/__DEFINES/ai/_ai.dm +++ b/code/__DEFINES/ai/_ai.dm @@ -223,7 +223,36 @@ #define BB_CAT_HOME "cat_home" /// key that holds the human we will beg #define BB_HUMAN_BEG_TARGET "human_beg_target" +#define BB_HUMAN_NPC_ATTACK_ZONE_COUNTER "human_npc_attack_zone_counter" +#define BB_HUMAN_NPC_LAST_ATTACK_ZONE "human_npc_last_attack_zone" +#define BB_HUMAN_NPC_WEAKPOINT "human_npc_weakpoint" +#define BB_HUMAN_NPC_JUMP_COOLDOWN "human_npc_jump_cooldown" +#define BB_HUMAN_NPC_FLANK_ANGLE "human_npc_flank_angle" +#define BB_HUMAN_NPC_FLANK_TARGET "human_npc_flank_target" +#define BB_HUMAN_NPC_HARASS_MODE "human_npc_harass_mode" +#define BB_HUMAN_NPC_HARASS_RETREATING "human_npc_harass_retreating" +#define BB_HUMAN_NPC_HARASS_COOLDOWN "human_npc_harass_cooldown" +#define BB_HUMAN_NPC_JUKE_COOLDOWN "human_npc_juke_cooldown" #define BB_BEGGING_FOOD_ITEM "item_beg_target" +#define BB_ARCHER_NPC_TARGET_ARROW "archer_target_arrow" +#define BB_ARCHER_NPC_STASHED_WEAPON "archer_stashed_weapon" +#define BB_ARCHER_NPC_EQUIPMENT_CACHE_EXPIRY "archer_npc_equipment_cache_expiry" +#define BB_ARCHER_NPC_BOW "archer_npc_bow" +#define BB_ARCHER_NPC_QUIVER "archer_npc_quiver" +#define BB_INVENTORY_MAP "inventory_map" // list(category = list(item_ref = slot_name)) +#define BB_CONTAINER_REFS "container_refs" // list(slot_name = item_ref) +#define BB_INVENTORY_DIRTY "inventory_dirty" // bool, triggers reappraisal +#define BB_HELD_CONSUMABLE "held_consumable" // item we pulled out to use +#define BB_TARGET_ZONE_OVERRIDE "bb_target_override" +#define BB_LOOT_TARGET "loot_target" +#define BB_LOOT_TARGET_ITEM "loot_target_item" +#define BB_LOOT_BLACKLIST "loot_blacklist" + +#define ARCHER_NPC_EQUIPMENT_CACHE_TIME (40 SECONDS) +#define ARCHER_NPC_MIN_RANGE 3 // tiles - closer than this, prefer melee +#define ARCHER_NPC_ARROW_SEARCH_RANGE 9 +#define ARCHER_NPC_SIMULATED_CHARGETIME 1.5 SECONDS // fallback charge wait in deciseconds + #define BB_CAT_KITTEN_TARGET "BB_cat_kitten_target" #define BB_CAT_HOLDING_FOOD "BB_cat_holding_food" @@ -298,3 +327,37 @@ #define ACTION_STATE_CONTINUE 1 #define ACTION_STATE_COMPLETE 2 #define ACTION_STATE_FAILED 3 + +#define AI_ITEM_BANDAGE (1<<0) // stops bleeding, applied to self/others +#define AI_ITEM_HEALING_DRINK (1<<1) // drinkable healing reagent container +#define AI_ITEM_FOOD (1<<2) // edible +#define AI_ITEM_POWDER (1<<3) // snortable /obj/item/reagent_containers/powder +#define AI_ITEM_KEY (1<<4) +#define AI_ITEM_TOOL (1<<5) +#define AI_ITEM_AMMO (1<<6) +#define AI_ITEM_GRENADE (1<<7) +#define AI_ITEM_MELEE (1<<8) +#define AI_ITEM_GUN (1<<9) +#define AI_ITEM_DRINK (1<<10) // generic drinkable (not necessarily healing) +#define AI_ITEM_THROWING (1<<11) +#define AI_ITEM_QUIVER (1<<12) + +GLOBAL_LIST_INIT(ai_item_flags, list( + AI_ITEM_BANDAGE, + AI_ITEM_HEALING_DRINK, + AI_ITEM_FOOD, + AI_ITEM_POWDER, + AI_ITEM_KEY, + AI_ITEM_TOOL, + AI_ITEM_AMMO, + AI_ITEM_GRENADE, + AI_ITEM_MELEE, + AI_ITEM_GUN, + AI_ITEM_DRINK, + AI_ITEM_THROWING, + AI_ITEM_QUIVER, +)) + +#define AI_INVENTORY_WATCHED_SLOTS (ITEM_SLOT_BELT | ITEM_SLOT_BACK_L | ITEM_SLOT_BACK_R | \ + ITEM_SLOT_BELT_L | ITEM_SLOT_BELT_R | ITEM_SLOT_ARMOR | ITEM_SLOT_PANTS | \ + ITEM_SLOT_SHIRT | ITEM_SLOT_CLOAK | ITEM_SLOT_BACK) diff --git a/code/__DEFINES/ai/hostile.dm b/code/__DEFINES/ai/hostile.dm index a8b4a4df270..d867e9bff62 100644 --- a/code/__DEFINES/ai/hostile.dm +++ b/code/__DEFINES/ai/hostile.dm @@ -60,7 +60,8 @@ ///list of foods this mob likes #define BB_BASIC_FOODS "BB_basic_foods" - +///What creature we want to cocoon +#define BB_BASIC_MOB_COCOON_TARGET "BB_basic_mob_cocoon_target" /// Flag to set on or off if you want your mob to prioritise running away #define BB_BASIC_MOB_FLEEING "BB_basic_fleeing" diff --git a/code/__DEFINES/animal_gene.dm b/code/__DEFINES/animal_gene.dm new file mode 100644 index 00000000000..9fe5a9cde99 --- /dev/null +++ b/code/__DEFINES/animal_gene.dm @@ -0,0 +1,37 @@ +#define GENE_GROUP_BODY_SIZE "body_size" +#define GENE_GROUP_SPEED "speed" +#define GENE_GROUP_CONSTITUTION "constitution" +#define GENE_GROUP_TEMPERAMENT "temperament" +#define GENE_GROUP_DIET "diet" +#define GENE_GROUP_HIDE "hide" +#define GENE_GROUP_COAT_COLOR "coat_color" +#define GENE_GROUP_UNDERCOAT "undercoat" +#define GENE_GROUP_BREEDING "breeding" +#define GENE_GROUP_PROGENY "progeny" +#define GENE_GROUP_EMISSIVE "emissive" + +#define GENETICS_TRAIT "genetics" +#define GENETICS_MUTATION_CHANCE 15 +#define GENETICS_EMERGENCE_CHANCE 40 +#define GENETICS_MAX_GENES 4 + +// How much random noise (+/-) is applied when averaging two parent intensities +#define GENETICS_INTENSITY_NOISE 1 +// Minimum fraction of intensity_min a bred gene can ever fall to +#define GENETICS_INTENSITY_FLOOR 9 +// Pass chance for dominant genes from a single parent +#define GENETICS_DOMINANT_PASS_CHANCE 70 +// Pass chance for recessive genes from a single parent, lower, needs both sides contributing to reliably transmit +#define GENETICS_RECESSIVE_PASS_CHANCE 40 + +#define RECESSIVE_NONE 0 +#define RECESSIVE_CARRIED 1 // one parent had it - silent carrier +#define RECESSIVE_EXPRESSED 2 // both parents had it - expresses + +#define GENE_FLAG_INTRINSIC (1<<0) // Always passed, doesn't count toward gene cap +#define GENE_FLAG_UNCOUNTED (1<<1) // This gene never gets counted towards the gene cap +#define GENE_FLAG_EXLUDE_WILD (1<<2) // This gene can never appear in the wild +#define GENE_FLAG_EMERGENCE (1<<3) // This gene will only appear through genetic mutation outside of the wild + +#define LINEAGE_MOTHER "mother" +#define LINEAGE_FATHER "father" diff --git a/code/__DEFINES/colors.dm b/code/__DEFINES/colors.dm index 6649de1b147..d7bf01ce4f4 100644 --- a/code/__DEFINES/colors.dm +++ b/code/__DEFINES/colors.dm @@ -111,6 +111,46 @@ #define CLOTHING_WHITE "#ffffff" #define CLOTHING_WET "#afafaf" +#define CLOTHING_RED "#8b2323" +#define CLOTHING_PURPLE "#8747b1" +#define CLOTHING_BLACK "#2b292e" +#define CLOTHING_GREY "#6c6c6c" +#define CLOTHING_BROWN "#61462c" +#define CLOTHING_GREEN "#428138" +#define CLOTHING_DARK_GREEN "#264d26" +#define CLOTHING_BLUE "#173266" +#define CLOTHING_YELLOW "#ffcd43" +#define CLOTHING_TEAL "#249589" +#define CLOTHING_AZURE "#007fff" +#define CLOTHING_ORANGE "#df8405" +#define CLOTHING_MAGENTA "#962e5c" + +//extended dye +#define CLOTHING_BURLAP "#a09571" +#define CLOTHING_CREAM "#fffdd0" +#define CLOTHING_DARK_GREY "#505050" +#define CLOTHING_DIRT "#7c6d5c" +#define CLOTHING_DUNKED_WATER "#bbbbbb" +#define CLOTHING_EGGPLANT "#5d4356" +#define CLOTHING_GOLD "#f9a602" +#define CLOTHING_GOLD_METALLIC "#b0955d" +#define CLOTHING_GULF_BLUE "#7bb6b0" +#define CLOTHING_LIGHT_GREY "#999999" +#define CLOTHING_MADDER "#d74c34" +#define CLOTHING_MAGE_GREY "#6c6c6c" +#define CLOTHING_MUDDY_YELLOW "#b5b004" +#define CLOTHING_OLIVE "#98bf64" +#define CLOTHING_ORCHIL "#66023c" +#define CLOTHING_PERIWINKLE_BLUE "#8f99fb" +#define CLOTHING_SCARLET "#b8252c" +#define CLOTHING_TAN "#d6a790" +#define CLOTHING_VIOLET "#5b2294" +#define CLOTHING_WOAD_BLUE "#597fb9" +#define CLOTHING_WISTERIA "#b07bb6" +#define CLOTHING_WINE_RED "#995264" +#define CLOTHING_YELLOW_WELD "#f4c430" +#define CLOTHING_YARROW "#f0cb76" + /// Deprecated macro, should be removed #define CLOTHING_COLOR_NAMES list("Ash Grey","Chalk White","Cream","White","Dark Ink","Plum Purple","Salmon","Blood Red", "Maroon","Red Ochre","Russet","Chestnut","Mustard Yellow","Yellow Ochre","Forest Green","Sky Blue","Teal", "Royal Black","Soot Black","Winestain Red","Royal Red","Royal Majenta","Fyritius Orange","Bark Brown","Peasant Brown","Mud Brown","Pear Yellow","Spring Green","Bog Green","Royal Teal","Berry Blue", "Royal Blue", "Royal Purple","Dunked in Water" ) @@ -124,6 +164,60 @@ #define CM_COLOR_LUM_MAX 0.75 +#define CLOTHING_COLOR_MAP list( \ + "Red" = CLOTHING_RED, \ + "Purple" = CLOTHING_PURPLE, \ + "Black" = CLOTHING_BLACK, \ + "Brown" = CLOTHING_BROWN, \ + "Green" = CLOTHING_GREEN, \ + "Blue" = CLOTHING_BLUE, \ + "Yellow" = CLOTHING_YELLOW, \ + "Teal" = CLOTHING_TEAL, \ + "Azure" = CLOTHING_AZURE, \ + "White" = CLOTHING_WHITE, \ + "Orange" = CLOTHING_ORANGE, \ + "Magenta" = CLOTHING_MAGENTA \ +) +/* Extended */ +#define EXTENDED_COLOR_MAP list( \ + "Burlap" = CLOTHING_BURLAP, \ + "Chalk White" = CLOTHING_CHALK_WHITE, \ + "Chestnut" = CLOTHING_CHESTNUT, \ + "Cream" = CLOTHING_CREAM, \ + "Dark Grey" = CLOTHING_DARK_GREY, \ + "Dirt" = CLOTHING_DIRT, \ + "Dunked in Water" = CLOTHING_DUNKED_WATER, \ + "Eggplant" = CLOTHING_EGGPLANT, \ + "Gold" = CLOTHING_GOLD, \ + "Gold Metallic" = CLOTHING_GOLD_METALLIC, \ + "Gulf Blue" = CLOTHING_GULF_BLUE, \ + "Light Grey" = CLOTHING_LIGHT_GREY, \ + "Madder" = CLOTHING_MADDER, \ + "Mage Blue" = CLOTHING_MAGE_BLUE, \ + "Mage Green" = CLOTHING_MAGE_GREEN, \ + "Mage Grey" = CLOTHING_MAGE_GREY, \ + "Mage Yellow" = CLOTHING_MAGE_YELLOW, \ + "Muddy Yellow" = CLOTHING_MUDDY_YELLOW, \ + "Maroon" = CLOTHING_MAROON, \ + "Olive" = CLOTHING_OLIVE, \ + "Orchil" = CLOTHING_ORCHIL, \ + "Peasant Brown" = CLOTHING_PEASANT_BROWN, \ + "Periwinkle Blue" = CLOTHING_PERIWINKLE_BLUE, \ + "Red Ochre" = CLOTHING_RED_OCHRE, \ + "Russet" = CLOTHING_RUSSET, \ + "Scarlet" = CLOTHING_SCARLET, \ + "Tan" = CLOTHING_TAN, \ + "Violet" = CLOTHING_VIOLET, \ + "Woad Blue" = CLOTHING_WOAD_BLUE, \ + "Wisteria" = CLOTHING_WISTERIA, \ + "Wine Red" = CLOTHING_WINE_RED, \ + "Yellow Ochre" = CLOTHING_YELLOW_OCHRE, \ + "Yellow Weld" = CLOTHING_YELLOW_WELD, \ + "Yarrow" = CLOTHING_YARROW \ +) + +#define COLOR_MAP (CLOTHING_COLOR_MAP + EXTENDED_COLOR_MAP) + /** * Gets a color for a name, will return the same color for a given string consistently within a round.atom * diff --git a/code/__DEFINES/components.dm b/code/__DEFINES/components.dm index 5bf3d604d6a..fa9046fa446 100644 --- a/code/__DEFINES/components.dm +++ b/code/__DEFINES/components.dm @@ -92,6 +92,11 @@ #define COMSIG_MOB_BREAK_SNEAK "mob_break_sneak" #define COMSIG_MOB_DEATH "mob_death" //from base of mob/death(): (gibbed) +#define COMSIG_MOB_TRY_BARK "try_bark" +#define COMSIG_MOB_TRY_EMOTE "try_emote" +#define COMSIG_MOB_MODIFY_AGGRO_LINES "comsig_mob_modify_aggro_lines" +#define COMSIG_MOB_MODIFY_DEATH_LINES "comsig_mob_modify_death_lines" + #define COMSIG_MOB_CREATED_CALLOUT "mob_created_callout" #define COMSIG_MOB_CLICKON "mob_clickon" //from base of mob/clickon(): (atom/A, params) @@ -125,6 +130,9 @@ #define SPEECH_IGNORE_SPAM 6 #define SPEECH_FORCED 7 */ +/// Called from the base of '/obj/item/bodypart/proc/drop_limb(special)' () +#define COMSIG_MOB_DISMEMBER "mob_drop_limb" + #define COMPONENT_CANCEL_DISMEMBER (1<<0) //cancel the drop limb #define COMSIG_MOB_DEADSAY "mob_deadsay" // from /mob/say_dead(): (mob/speaker, message) #define MOB_DEADSAY_SIGNAL_INTERCEPT 1 ///from base of /mob/verb/pointed: (atom/A) @@ -164,7 +172,7 @@ #define COMSIG_MACHINERY_POWER_LOST "machinery_power_lost" //from base power_change() when power is lost #define COMSIG_MACHINERY_POWER_RESTORED "machinery_power_restored" //from base power_change() when power is restored - +#define COMSIG_MOB_DROPITEM "mob_dropitem" /// A mob has just equipped an item. Called on [/mob] from base of [/obj/item/equipped()]: (/obj/item/equipped_item, slot) #define COMSIG_MOB_EQUIPPED_ITEM "mob_equipped_item" /// A mob has just unequipped an item. diff --git a/code/__DEFINES/danger.dm b/code/__DEFINES/danger.dm new file mode 100644 index 00000000000..bd5e949564c --- /dev/null +++ b/code/__DEFINES/danger.dm @@ -0,0 +1,24 @@ + +#define DANGER_SAFE_FLOOR 0 +#define DANGER_SAFE_LIMIT 10 +#define DANGER_LOW_FLOOR 11 +#define DANGER_LOW_LIMIT 20 +#define DANGER_MODERATE_FLOOR 21 +#define DANGER_MODERATE_LIMIT 30 +#define DANGER_DANGEROUS_FLOOR 31 +#define DANGER_DANGEROUS_LIMIT 40 +#define DANGER_DIRE_FLOOR 41 +#define DANGER_DIRE_LIMIT 60 + +#define DANGER_LEVEL_SAFE "Safe" +#define DANGER_LEVEL_LOW "Low" +#define DANGER_LEVEL_MODERATE "Moderate" +#define DANGER_LEVEL_DANGEROUS "Dangerous" +#define DANGER_LEVEL_BLEAK "Bleak" + +#define THREAT_REGION_BASIN "Basin" +#define THREAT_REGION_NORTHERN_GROVE "Northern Grove" +#define THREAT_REGION_OUTER_GROVE "Outer Grove" // Grove west of the road +#define THREAT_REGION_MOUNT_DECAP "Mount Decapitation" +#define THREAT_REGION_TERRORBOG "Terrorbog" +#define THREAT_REGION_COAST "Coast" diff --git a/code/__DEFINES/obj_flags.dm b/code/__DEFINES/obj_flags.dm index 29f482a51b5..e797a85ef38 100644 --- a/code/__DEFINES/obj_flags.dm +++ b/code/__DEFINES/obj_flags.dm @@ -33,6 +33,7 @@ #define SURGICAL_TOOL (1<<12) //Tool commonly used for surgery: won't attack targets in an active surgical operation on help intent (in case of mistakes) #define SHRINK_ENCHANT (1<<13) #define ITEM_ONLY_BREAK (1<<14) +#define HIGH_VALUE (1<<15) // Flags for the clothing_flags var on /obj/item/clothing diff --git a/code/__DEFINES/quests.dm b/code/__DEFINES/quests.dm new file mode 100644 index 00000000000..561ca21d3e1 --- /dev/null +++ b/code/__DEFINES/quests.dm @@ -0,0 +1,59 @@ +#define QUEST_DIFFICULTY_EASY "Easy" +#define QUEST_DIFFICULTY_MEDIUM "Medium" +#define QUEST_DIFFICULTY_HARD "Hard" + +#define QUEST_RETRIEVAL "Retrieval" +#define QUEST_COURIER "Courier" +#define QUEST_KILL_EASY "Kill" +#define QUEST_CLEAR_OUT "Clear Out" +#define QUEST_RAID "Raid" +#define QUEST_OUTLAW "Outlaw" +#define QUEST_BEACON "Beacon" + +#define QUEST_REWARD_EASY_LOW 30 +#define QUEST_REWARD_EASY_HIGH 35 +#define QUEST_REWARD_MEDIUM_LOW 45 +#define QUEST_REWARD_MEDIUM_HIGH 65 +#define QUEST_REWARD_HARD_LOW 120 +#define QUEST_REWARD_HARD_HIGH 180 + +#define QUEST_DEPOSIT_EASY 5 +#define QUEST_DEPOSIT_MEDIUM 10 +#define QUEST_DEPOSIT_HARD 20 + +#define QUEST_HANDLER_REWARD_MULTIPLIER 2 + +// Delivery quest additional reward scaling +#define QUEST_DELIVERY_DISTANCE_DIVISOR 8 // Divides the distance for reward calculation +#define QUEST_DELIVERY_DISTANCE_BONUS 1 // Adds a bonus for longer distances +#define QUEST_COURIER_BONUS_FLAT 10 // Flat bonus for courier quests, since you gotta wait for a person to open a package +#define QUEST_DELIVERY_PER_ITEM_BONUS 2 // Bonus per item delivered + +// All eligible quest kill mobs +// The extra per number reward are based on toughness + whether their head is worth anything +#define QUEST_KILL_MOBS_LIST list(\ + /mob/living/carbon/human/species/goblin/npc/ambush/sea = 3,\ + /mob/living/carbon/human/species/skeleton/npc/supereasy = 4,\ + /mob/living/carbon/human/species/skeleton/npc/easy = 5,\ + /mob/living/carbon/human/species/skeleton/npc/pirate = 5,\ + /mob/living/carbon/human/species/human/northern/militia/deserter = 4,\ + /mob/living/carbon/human/species/orc/npc/footsoldier = 6,\ +) + +// Medium difficulty quest kill mobs, this is where I can put some slightly spicier mobs +#define QUEST_KILL_MEDIUM_LIST list(\ + /mob/living/carbon/human/species/human/northern/searaider/ambush = 6,\ + /mob/living/carbon/human/species/human/northern/highwayman = 6,\ + /mob/living/carbon/human/species/orc/npc/footsoldier = 6,\ + /mob/living/carbon/human/species/orc/npc/marauder = 8,\ + /mob/living/carbon/human/species/skeleton/npc/mediumspread = 6,\ + /mob/living/carbon/human/species/skeleton/npc/mediumspread = 6,\ + /mob/living/carbon/human/species/human/northern/thief = 8,\ + ) + +// Raid difficulty kill mobs - Only three mobs for now. Per person reward is low because base / head reward is high +#define QUEST_RAID_LIST list(\ + /mob/living/carbon/human/species/orc/npc/berserker = 10,\ + /mob/living/carbon/human/species/elf/dark/drowraider = 5, \ + /mob/living/carbon/human/species/human/northern/bog_deserters = 5,\ +) diff --git a/code/__DEFINES/traits/definitions.dm b/code/__DEFINES/traits/definitions.dm index 849c1f5a10c..e209d898cd8 100644 --- a/code/__DEFINES/traits/definitions.dm +++ b/code/__DEFINES/traits/definitions.dm @@ -70,6 +70,7 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define TRAIT_DEATHCOMA "deathcoma" /// ??? should be a signal? #define TRAIT_SANGUINE "sanguine" +#define TRAIT_FRESHSPAWN "freshspawn" /// The mob has the stasis effect. /// Does nothing on its own, applied via status effect. #define TRAIT_STASIS "in_stasis" @@ -226,6 +227,8 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai #define TRAIT_BLUEPRINT_VISION "blueprint_vision" /// Used to limit healing to putrid flesh mobs #define TRAIT_PUTRID "Putrid" +#define TRAIT_STUCKITEMS "stuck_items" // Prevents removing items except for hand slots +#define TRAIT_HIGHVALUE_STUCK "highvalue_stuck" //Prevents removing items except for hand slots if it is consdiered to strong /// Confessed under torture, to force sign #define TRAIT_HAS_CONFESSED "has_confessed" /// Confessed for specific type of antag @@ -526,3 +529,7 @@ Remember to update _globalvars/traits.dm if you're adding/removing/renaming trai /// This object has sound debugging tools attached to it #define TRAIT_SOUND_DEBUGGED "sound_debugged" + +// genetic traits +#define TRAIT_ANIMAL_NATURAL_ARMOR "natural_armor" +#define TRAIT_ANIMAL_PRODUCTIVE "trait_productive" diff --git a/code/__HELPERS/_logging.dm b/code/__HELPERS/_logging.dm index bfb95e89d9d..192c52af196 100644 --- a/code/__HELPERS/_logging.dm +++ b/code/__HELPERS/_logging.dm @@ -104,6 +104,10 @@ if(CONFIG_GET(flag/log_manifest)) WRITE_LOG(GLOB.world_manifest_log, "\[[TIMETOTEXT4LOGS]\] [ckey] \\ [body.real_name] \\ [mind.assigned_role.title] \\ [mind.special_role ? mind.special_role : "NONE"] \\ [latejoin ? "LATEJOIN":"ROUNDSTART"]") +/proc/log_quest(ckey, datum/mind/mind, mob/body, text) + if (CONFIG_GET(flag/log_law)) + WRITE_LOG(GLOB.world_game_log, "\[[TIMETOTEXT4LOGS]] QUEST: [ckey] \\ [body.real_name] \\ [text]") + /proc/log_bomber(atom/user, details, atom/bomb, additional_details, message_admins = TRUE) var/bomb_message = "\[[TIMETOTEXT4LOGS]\] [details][bomb ? " [bomb.name] at [AREACOORD(bomb)]": ""][additional_details ? " [additional_details]" : ""]." diff --git a/code/__HELPERS/directions.dm b/code/__HELPERS/directions.dm new file mode 100644 index 00000000000..56a5ed738c0 --- /dev/null +++ b/code/__HELPERS/directions.dm @@ -0,0 +1,59 @@ +/// Get the direction between one atom to another in precise compass terms (North-northwest etc.) +/proc/get_precise_direction_between(atom/from_atom, atom/to_atom) + var/turf/from_turf = get_turf(from_atom) + var/turf/to_turf = get_turf(to_atom) + if(!from_turf || !to_turf) + return null + + var/dx = to_turf.x - from_turf.x + var/dy = to_turf.y - from_turf.y + if(!dx && !dy) + return null + + var/angle = ATAN2(dx, dy) + if(angle < 0) + angle += 360 + return get_precise_direction_from_angle(angle) + +/proc/get_precise_direction_from_angle(angle) + // Normalize the incoming angle first. + angle = (angle + 360) % 360 + + // Convert to compass bearing (0° = north, 90° = east). + var/compass_angle = (450 - angle) % 360 // 450 = 360 + 90 + + switch(compass_angle) + if(348.75 to 360, 0 to 11.25) + return "north" + if(11.25 to 33.75) + return "north-northeast" + if(33.75 to 56.25) + return "northeast" + if(56.25 to 78.75) + return "east-northeast" + if(78.75 to 101.25) + return "east" + if(101.25 to 123.75) + return "east-southeast" + if(123.75 to 146.25) + return "southeast" + if(146.25 to 168.75) + return "south-southeast" + if(168.75 to 191.25) + return "south" + if(191.25 to 213.75) + return "south-southwest" + if(213.75 to 236.25) + return "southwest" + if(236.25 to 258.75) + return "west-southwest" + if(258.75 to 281.25) + return "west" + if(281.25 to 303.75) + return "west-northwest" + if(303.75 to 326.25) + return "northwest" + if(326.25 to 348.75) + return "north-northwest" + + return null diff --git a/code/_globalvars/lists/mapping.dm b/code/_globalvars/lists/mapping.dm index eabcbf3ed5f..09f33c86b30 100644 --- a/code/_globalvars/lists/mapping.dm +++ b/code/_globalvars/lists/mapping.dm @@ -17,6 +17,7 @@ GLOBAL_LIST_EMPTY(start_landmarks_list) //list of all spawn points created GLOBAL_LIST_EMPTY(department_security_spawns) //list of all department security spawns GLOBAL_LIST_EMPTY(generic_event_spawns) //handles clockwork portal+eminence teleport destinations GLOBAL_LIST_EMPTY(jobspawn_overrides) //These will take precedence over normal spawnpoints if created. +GLOBAL_LIST_EMPTY(quest_landmarks_list) GLOBAL_LIST_EMPTY(lich_starts) GLOBAL_LIST_EMPTY(bandit_starts) diff --git a/code/_onclick/item_attack.dm b/code/_onclick/item_attack.dm index 489bdb620ef..30c304d5608 100644 --- a/code/_onclick/item_attack.dm +++ b/code/_onclick/item_attack.dm @@ -676,11 +676,78 @@ return TRUE /mob/living/simple_animal/attacked_by(obj/item/I, mob/living/user) - if(I.force < force_threshold || I.damtype == STAMINA) - playsound(src, 'sound/blank.ogg', I.get_clamped_volume(), TRUE, -1) - else - . = ..() - I.do_special_attack_effect(user, null, null, src, null) + var/hitlim = simple_limb_hit(user.zone_selected) + I.funny_attack_effects(src, user) + var/newforce = get_complex_damage(I, user) + var/haha = user.used_intent.blade_class + var/armor = run_armor_check(null, haha, armor_penetration = I.armor_penetration, damage = newforce) + var/nodmg = FALSE + next_attack_msg.Cut() + if(armor > 0) + nodmg = TRUE + next_attack_msg += span_warning("Armor stops the damage.") + apply_damage(newforce, I.damtype, hitlim, armor) + I.remove_bintegrity(1) + if(I.damtype == BRUTE && !nodmg) + if(HAS_TRAIT(src, TRAIT_SIMPLE_WOUNDS)) + simple_woundcritroll(user.used_intent.blade_class, newforce, user, hitlim) + if(newforce > 5) + if(haha != BCLASS_BLUNT) + I.add_mob_blood(src) + var/turf/location = get_turf(src) + add_splatter_floor(location) + if(get_dist(user, src) <= 1) //people with TK won't get smeared with blood + user.add_mob_blood(src) + if(newforce > 15) + if(haha == BCLASS_BLUNT) + I.add_mob_blood(src) + var/turf/location = get_turf(src) + add_splatter_floor(location) + if(get_dist(user, src) <= 1) //people with TK won't get smeared with blood + user.add_mob_blood(src) + send_item_attack_message(I, user, hitlim) + next_attack_msg.Cut() + I.do_special_attack_effect(user, null, null, src, null) + + +/mob/living/simple_animal/getarmor(def_zone, type, damage, armor_penetration, blade_dulling, peeldivisor, intdamfactor = 1, used_weapon) + if(!type) + return 0 + var/armorval = 0 + if(HAS_TRAIT(src, TRAIT_ANIMAL_NATURAL_ARMOR) && genetics) + var/natural = genetics.get_natural_armor_for_type(type) + if(natural) + armorval += max(0, natural - armor_penetration) + + if(bbarding && !bbarding.obj_broken) + armorval = bbarding.armor.getRating(type) + var/intdamage = damage + if(type != "blunt") + if((damage + armor_penetration) > armorval) + intdamage = (damage + armor_penetration) - armorval + + if(intdamfactor != 1) + intdamage *= intdamfactor + + bbarding.take_damage(intdamage, damage_flag = type, sound_effect = FALSE, armor_penetration = 100) + else + if(mind) + if(armorval > 0) + intdamage -= intdamage * ((armorval / 1.66) / 100) //Reduces it up to 60% (100 dmg -> 40 dmg at Blunt S armor (100)) + if(intdamfactor != 1) + intdamage *= intdamfactor + + bbarding.take_damage(intdamage, damage_flag = type, sound_effect = FALSE, armor_penetration = 100) + + return armorval + +/mob/living/simple_animal/damage_clothes(damage_amount, damage_type = BRUTE, damage_flag = 0, def_zone) + if(damage_type != BRUTE && damage_type != BURN) + return + if(!bbarding) + return + damage_amount *= 0.5 //0.5 multiplier for balance reason, we don't want clothes to be too easily destroyed + bbarding.take_damage(damage_amount, damage_type, damage_flag, 0) /** * Last proc in the [/obj/item/proc/melee_attack_chain] diff --git a/code/controllers/subsystem/regional_threat.dm b/code/controllers/subsystem/regional_threat.dm new file mode 100644 index 00000000000..b7c117e7e0c --- /dev/null +++ b/code/controllers/subsystem/regional_threat.dm @@ -0,0 +1,46 @@ +SUBSYSTEM_DEF(regionthreat) + name = "Regional Threat" + wait = 15 MINUTES + flags = SS_KEEP_TIMING | SS_BACKGROUND + runlevels = RUNLEVEL_GAME + var/list/threat_regions = list() + +/datum/controller/subsystem/regionthreat/Initialize(start_timeofday) + for(var/datum/threat_region/region as anything in subtypesof(/datum/threat_region)) + if(IS_ABSTRACT(region)) + continue + threat_regions += new region() + return ..() + +/datum/controller/subsystem/regionthreat/fire(resumed) + var/player_count = GLOB.player_list.len + var/ishighpop = player_count >= LOWPOP_THRESHOLD + for(var/T in threat_regions) + var/datum/threat_region/TR = T + if(ishighpop) + TR.increase_latent_ambush(TR.highpop_tick) + else + TR.increase_latent_ambush(TR.lowpop_tick) + +/datum/controller/subsystem/regionthreat/proc/get_region(region_name) + for(var/T in threat_regions) + var/datum/threat_region/TR = T + if(TR.region_name == region_name) + return TR + return null + +/datum/threat_region_display + var/region_name + var/danger_level + var/danger_color + +/datum/controller/subsystem/regionthreat/proc/get_threat_regions_for_display() + var/list/threat_region_displays = list() + for(var/T in threat_regions) + var/datum/threat_region/TR = T + var/datum/threat_region_display/TRS = new /datum/threat_region_display + TRS.region_name = TR.region_name + TRS.danger_level = TR.get_danger_level() + TRS.danger_color = TR.get_danger_color() + threat_region_displays += TRS + return threat_region_displays diff --git a/code/controllers/subsystem/ticker.dm b/code/controllers/subsystem/ticker.dm index f9303a057bd..1eb65bbbf7f 100644 --- a/code/controllers/subsystem/ticker.dm +++ b/code/controllers/subsystem/ticker.dm @@ -75,7 +75,9 @@ SUBSYSTEM_DEF(ticker) var/mob/living/carbon/human/rulermob = null /// The appointed regent mob var/mob/living/carbon/human/regent_mob = null - var/failedstarts = 0 + var/vote_started = FALSE + var/voting = FALSE + var/pre_vote = 0 var/list/manualmodes = list() var/end_party = FALSE @@ -305,9 +307,14 @@ SUBSYSTEM_DEF(ticker) continue readied_jobs.Add(V) - if(CONFIG_GET(flag/ruler_required)) + if(CONFIG_GET(flag/ruler_required) && !vote_started) + if(pre_vote > 4 && !voting) + voting = TRUE + SSvote.initiate_vote("norulervote", "The Gods") if(!(("Monarch" in readied_jobs) || (start_immediately == TRUE))) //start_immediately triggers when the world is doing a test run or an admin hits start now, we don't need to check for king to_chat(world, span_purple("[pick(no_ruler_lines)]")) + if(!voting) + pre_vote++ return FALSE job_change_locked = TRUE diff --git a/code/controllers/subsystem/vote.dm b/code/controllers/subsystem/vote.dm index eb62fa09dbc..60887d19355 100644 --- a/code/controllers/subsystem/vote.dm +++ b/code/controllers/subsystem/vote.dm @@ -190,6 +190,13 @@ SUBSYSTEM_DEF(vote) if("storyteller") SSgamemode.storyteller_vote_result(.) + if("norulervote") + switch(.) + if("Start Anyway") + SSticker.vote_started = TRUE + if("Wait for Ruler") + SSticker.vote_started = FALSE + SSticker.pre_vote = 0 if(restart) var/active_admins = 0 for(var/client/C in GLOB.admins) @@ -280,6 +287,8 @@ SUBSYSTEM_DEF(vote) choices.Add("Continue Playing","End Round") if("storyteller") choices.Add(SSgamemode.storyteller_vote_choices()) + if("norulervote") + choices.Add("Start Anyway", "Wait for Ruler") else return 0 mode = vote_type @@ -305,6 +314,11 @@ SUBSYSTEM_DEF(vote) return 1 return 0 +/datum/controller/subsystem/vote/proc/initiate_norulervote() + if(mode) // Already a vote in progress + return 0 + return initiate_vote("norulervote", "The Gods") + /datum/controller/subsystem/vote/proc/interface(client/C) if(!C) return diff --git a/code/datums/ai/_ai_controller.dm b/code/datums/ai/_ai_controller.dm index 493fd210856..5addf3879e5 100644 --- a/code/datums/ai/_ai_controller.dm +++ b/code/datums/ai/_ai_controller.dm @@ -47,6 +47,8 @@ have ways of interacting with a specific atom and control it. They posses a blac // Movement related things here ///Reference to the movement datum we use. Is a type on initialize but becomes a ref afterwards. var/datum/ai_movement/ai_movement = /datum/ai_movement/dumb + /// this shouldn't be a component tbh but uh reusing code go brrr + var/datum/component/ai_inventory_manager/inventory_component ///Cooldown until next movement COOLDOWN_DECLARE(movement_cooldown) ///Delay between movements. This is on the controller so we can keep the movement datum singleton @@ -82,6 +84,7 @@ have ways of interacting with a specific atom and control it. They posses a blac /datum/ai_controller/Destroy(force, ...) UnpossessPawn(FALSE) our_cells = null + inventory_component = null set_movement_target(type, null) if(ai_movement.moving_controllers[src]) ai_movement.stop_moving_towards(src) @@ -102,6 +105,45 @@ have ways of interacting with a specific atom and control it. They posses a blac if(new_movement) change_ai_movement_type(new_movement) + +/** + * Removes a subtree from planning_subtrees by typepath. + * Safe to call whether planning_subtrees holds instances or typepaths. + */ +/datum/ai_controller/proc/remove_subtree(datum/ai_planning_subtree/subtree_type) + for(var/datum/ai_planning_subtree/subtree as anything in planning_subtrees) + if(subtree.type == subtree_type) + planning_subtrees -= subtree + return + +/** + * Adds a subtree at a given position (1-indexed) by typepath, resolving the singleton instance. + * If the subtree is already present it will not be added again. + * Position is clamped so 1 = top, length+1 (or any value beyond the list) = bottom. + */ +/datum/ai_controller/proc/add_subtree_at(datum/ai_planning_subtree/subtree_type, index = 1) + for(var/datum/ai_planning_subtree/subtree as anything in planning_subtrees) + if(subtree.type == subtree_type) + return // already present, do nothing + + var/datum/ai_planning_subtree/subtree_instance = GLOB.ai_subtrees[subtree_type] + if(!subtree_instance) + CRASH("add_subtree_at: subtree type [subtree_type] not found in GLOB.ai_subtrees") + + LAZYINITLIST(planning_subtrees) + index = clamp(index, 1, length(planning_subtrees) + 1) + planning_subtrees.Insert(index, subtree_instance) + +/** + * Returns the index of a subtree in planning_subtrees by typepath, or 0 if not found. + */ +/datum/ai_controller/proc/get_subtree_index(datum/ai_planning_subtree/subtree_type) + for(var/i in 1 to length(planning_subtrees)) + var/datum/ai_planning_subtree/subtree = planning_subtrees[i] + if(subtree.type == subtree_type) + return i + return 0 + ///Overrides the current ai_movement of this controller with a new one /datum/ai_controller/proc/change_ai_movement_type(datum/ai_movement/new_movement) ai_movement = SSai_movement.movement_types[new_movement] @@ -168,7 +210,7 @@ have ways of interacting with a specific atom and control it. They posses a blac return !QDELETED(pawn) ///Interact with objects -/datum/ai_controller/proc/ai_interact(target, combat_mode, nextmove = FALSE, list/modifiers) +/datum/ai_controller/proc/ai_interact(target, combat_mode, nextmove = FALSE, list/modifiers, maintain_position = FALSE) if(!ai_can_interact()) return FALSE @@ -181,10 +223,11 @@ have ways of interacting with a specific atom and control it. They posses a blac if(nextmove && living_pawn.next_move > world.time) return FALSE - if(living_pawn.body_position == LYING_DOWN) - living_pawn.aimheight_change(rand(1,9)) - else - living_pawn.aimheight_change(rand(10,19)) + if(!maintain_position) + if(living_pawn.body_position == LYING_DOWN) + living_pawn.aimheight_change(rand(1,9)) + else + living_pawn.aimheight_change(rand(10,19)) var/params = list2params(modifiers) @@ -513,7 +556,16 @@ have ways of interacting with a specific atom and control it. They posses a blac behavior_args -= behavior_type SEND_SIGNAL(src, AI_CONTROLLER_BEHAVIOR_QUEUED(behavior_type), arguments) +/datum/ai_controller/proc/get_inventory() + RETURN_TYPE(/datum/component/ai_inventory_manager) + if(!inventory_component) + return pawn?.GetComponent(/datum/component/ai_inventory_manager) + return inventory_component + /datum/ai_controller/proc/ProcessBehavior(delta_time, datum/ai_behavior/behavior) + var/mob/living/liver = pawn + if(liver.doing()) + return var/list/arguments = list(delta_time, src) var/list/stored_arguments = behavior_args[behavior.type] if(stored_arguments) @@ -788,25 +840,30 @@ have ways of interacting with a specific atom and control it. They posses a blac while(index <= length(remove_queue)) var/list/next_to_clear = remove_queue[index] for(var/inner_value in next_to_clear) - var/associated_value = next_to_clear[inner_value] - // We are a lists of lists, add the next value to the queue so we can handle references in there - // (But we only need to bother checking the list if it's not empty.) - if(islist(inner_value) && length(inner_value)) - UNTYPED_LIST_ADD(remove_queue, inner_value) - - // We found the value that's been deleted. Clear it out from this list - else if(inner_value == source) - next_to_clear -= inner_value - - // We are an assoc lists of lists, the list at the next value so we can handle references in there - // (But again, we only need to bother checking the list if it's not empty.) - if(islist(associated_value) && length(associated_value)) - UNTYPED_LIST_ADD(remove_queue, associated_value) - - // We found the value that's been deleted, it was an assoc value. Clear it out entirely - else if(associated_value == source) - next_to_clear -= inner_value - SEND_SIGNAL(pawn, COMSIG_AI_BLACKBOARD_KEY_CLEARED(inner_value)) + if(isnum(inner_value)) + if(inner_value == source) + next_to_clear -= inner_value + SEND_SIGNAL(pawn, COMSIG_AI_BLACKBOARD_KEY_CLEARED(inner_value)) + else + var/associated_value = next_to_clear[inner_value] + // We are a lists of lists, add the next value to the queue so we can handle references in there + // (But we only need to bother checking the list if it's not empty.) + if(islist(inner_value) && length(inner_value)) + UNTYPED_LIST_ADD(remove_queue, inner_value) + + // We found the value that's been deleted. Clear it out from this list + else if(inner_value == source) + next_to_clear -= inner_value + + // We are an assoc lists of lists, the list at the next value so we can handle references in there + // (But again, we only need to bother checking the list if it's not empty.) + if(islist(associated_value) && length(associated_value)) + UNTYPED_LIST_ADD(remove_queue, associated_value) + + // We found the value that's been deleted, it was an assoc value. Clear it out entirely + else if(associated_value == source) + next_to_clear -= inner_value + SEND_SIGNAL(pawn, COMSIG_AI_BLACKBOARD_KEY_CLEARED(inner_value)) index += 1 diff --git a/code/datums/ai/behaviours/find_and_set.dm b/code/datums/ai/behaviours/find_and_set.dm index 8b9f561dea9..7e2eddff5db 100644 --- a/code/datums/ai/behaviours/find_and_set.dm +++ b/code/datums/ai/behaviours/find_and_set.dm @@ -598,7 +598,7 @@ GLOBAL_LIST_INIT(find_and_set_interested_atoms, typecacheof(list(/obj/item, /mob var/datum/proximity_monitor/field = controller.blackboard[BB_FIND_TARGETS_FIELD(type)] qdel(field) // autoclears so it's fine controller.CancelActions() - controller.modify_cooldown(src, get_cooldown(controller)) + controller.modify_cooldown(src, world.time + get_cooldown(controller)) /** * Proximity monitor for find_and_set tracking diff --git a/code/datums/ai/behaviours/hostile/find_highest_aggro.dm b/code/datums/ai/behaviours/hostile/find_highest_aggro.dm index 1bcc42cee57..a3014f71c56 100644 --- a/code/datums/ai/behaviours/hostile/find_highest_aggro.dm +++ b/code/datums/ai/behaviours/hostile/find_highest_aggro.dm @@ -124,6 +124,8 @@ finish_action(controller, succeeded = FALSE) /datum/ai_behavior/find_aggro_targets/proc/failed_to_find_anyone(datum/ai_controller/controller, target_key, targeting_strategy_key, hiding_location_key) + if(HAS_TRAIT(controller.pawn, TRAIT_FRESHSPAWN)) + return var/aggro_range = controller.blackboard[BB_AGGRO_RANGE] || 9 // takes the larger between our range() input and our implicit hearers() input (world.view) aggro_range = max(aggro_range, ROUND_UP(max(getviewsize(world.view)) / 2)) @@ -189,14 +191,12 @@ if(!accepted_targets.len) return - // Add threat to all accepted targets, then see if any become our new highest threat var/datum/component/ai_aggro_system/aggro_comp = pawn.GetComponent(/datum/component/ai_aggro_system) if(aggro_comp) for(var/mob/living/target in accepted_targets) aggro_comp.add_threat_to_mob_capped(target, 15, 15) aggro_comp.add_threat_to_mob(target, 3) - // Check if we now have a highest threat target var/mob/highest_threat = controller.blackboard[BB_HIGHEST_THREAT_MOB] if(highest_threat) controller.set_blackboard_key(target_key, highest_threat) @@ -206,6 +206,8 @@ controller.set_blackboard_key(hiding_location_key, potential_hiding_location) finish_action(controller, succeeded = TRUE) + else + controller.modify_cooldown(src, world.time) /// Helper proc to find if a mob is hiding in something /datum/ai_behavior/find_aggro_targets/proc/find_hiding_location(mob/living/source, mob/living/target) @@ -220,7 +222,7 @@ var/datum/proximity_monitor/field = controller.blackboard[BB_FIND_TARGETS_FIELD(type)] qdel(field) // autoclears so it's fine controller.CancelActions() // Cancel any further queued actions so they setup again with new target - controller.modify_cooldown(controller, get_cooldown(controller)) + controller.modify_cooldown(controller, world.time + get_cooldown(controller)) /datum/ai_behavior/find_aggro_targets/bum/finish_action(datum/ai_controller/controller, succeeded, ...) . = ..() diff --git a/code/datums/ai/behaviours/hostile/find_potential_targets.dm b/code/datums/ai/behaviours/hostile/find_potential_targets.dm index e06d6f75860..8f2c80e5a1c 100644 --- a/code/datums/ai/behaviours/hostile/find_potential_targets.dm +++ b/code/datums/ai/behaviours/hostile/find_potential_targets.dm @@ -14,6 +14,7 @@ GLOBAL_LIST_INIT(target_interested_atoms, typecacheof(list(/mob))) return ..() /datum/ai_behavior/find_potential_targets/perform(seconds_per_tick, datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key) + . = ..() var/mob/living/living_mob = controller.pawn if(living_mob.pet_passive) finish_action(controller, succeeded = FALSE) @@ -151,7 +152,7 @@ GLOBAL_LIST_INIT(target_interested_atoms, typecacheof(list(/mob))) var/datum/proximity_monitor/field = controller.blackboard[BB_FIND_TARGETS_FIELD(type)] qdel(field) // autoclears so it's fine controller.CancelActions() // On retarget cancel any further queued actions so that they will setup again with new target - controller.modify_cooldown(controller, get_cooldown(controller)) + controller.modify_cooldown(controller, world.time + get_cooldown(controller)) /// Returns the desired final target from the filtered list of targets /datum/ai_behavior/find_potential_targets/proc/pick_final_target(datum/ai_controller/controller, list/filtered_targets) diff --git a/code/datums/ai/behaviours/interact_with_target.dm b/code/datums/ai/behaviours/interact_with_target.dm index 872dc29aea2..3f48fb51ceb 100644 --- a/code/datums/ai/behaviours/interact_with_target.dm +++ b/code/datums/ai/behaviours/interact_with_target.dm @@ -14,6 +14,7 @@ set_movement_target(controller, target) /datum/ai_behavior/interact_with_target/perform(seconds_per_tick, datum/ai_controller/controller, target_key) + . = ..() var/atom/target = controller.blackboard[target_key] if(QDELETED(target) || !pre_interact(controller, target)) finish_action(controller, FALSE) diff --git a/code/datums/ai/behaviours/move_to_cardinal.dm b/code/datums/ai/behaviours/move_to_cardinal.dm index 18886f4e40f..2047c0f4200 100644 --- a/code/datums/ai/behaviours/move_to_cardinal.dm +++ b/code/datums/ai/behaviours/move_to_cardinal.dm @@ -36,6 +36,7 @@ set_movement_target(controller, move_target) /datum/ai_behavior/move_to_cardinal/perform(seconds_per_tick, datum/ai_controller/controller, target_key) + . = ..() var/atom/target = controller.blackboard[target_key] if (QDELETED(target)) finish_action(controller = controller, succeeded = FALSE, target_key = target_key) diff --git a/code/datums/ai/behaviours/perform_emote.dm b/code/datums/ai/behaviours/perform_emote.dm index 72aafc53c7b..b1980da64b6 100644 --- a/code/datums/ai/behaviours/perform_emote.dm +++ b/code/datums/ai/behaviours/perform_emote.dm @@ -1,6 +1,7 @@ /datum/ai_behavior/perform_emote /datum/ai_behavior/perform_emote/perform(delta_time, datum/ai_controller/controller, emote) + . = ..() var/mob/living/living_pawn = controller.pawn if(!istype(living_pawn)) return diff --git a/code/datums/ai/behaviours/perform_speech.dm b/code/datums/ai/behaviours/perform_speech.dm index c1e87459fac..1c89729ef90 100644 --- a/code/datums/ai/behaviours/perform_speech.dm +++ b/code/datums/ai/behaviours/perform_speech.dm @@ -1,6 +1,7 @@ /datum/ai_behavior/perform_speech /datum/ai_behavior/perform_speech/perform(delta_time, datum/ai_controller/controller, speech) + . = ..() var/mob/living/living_pawn = controller.pawn if(!istype(living_pawn)) return diff --git a/code/datums/ai/behaviours/pet_use_ability.dm b/code/datums/ai/behaviours/pet_use_ability.dm index 6936ad323cf..e9fca4dd517 100644 --- a/code/datums/ai/behaviours/pet_use_ability.dm +++ b/code/datums/ai/behaviours/pet_use_ability.dm @@ -10,6 +10,7 @@ set_movement_target(controller, target) /datum/ai_behavior/pet_use_ability/perform(seconds_per_tick, datum/ai_controller/controller, ability_key, target_key) + . = ..() var/datum/action/cooldown/ability = controller.blackboard[ability_key] var/mob/living/target = controller.blackboard[target_key] if (QDELETED(ability) || QDELETED(target)) diff --git a/code/datums/ai/behaviours/use_targeted_ability.dm b/code/datums/ai/behaviours/use_targeted_ability.dm index 3e0d196be7b..303d891c8ac 100644 --- a/code/datums/ai/behaviours/use_targeted_ability.dm +++ b/code/datums/ai/behaviours/use_targeted_ability.dm @@ -5,6 +5,7 @@ /datum/ai_behavior/targeted_mob_ability /datum/ai_behavior/targeted_mob_ability/perform(seconds_per_tick, datum/ai_controller/controller, ability_key, target_key) + . = ..() var/datum/action/cooldown/ability = controller.blackboard[ability_key] var/mob/living/target = controller.blackboard[target_key] if(QDELETED(ability) || QDELETED(target)) diff --git a/code/datums/ai/controllers/human_npc.dm b/code/datums/ai/controllers/human_npc.dm index 942db798a49..e93afd06430 100644 --- a/code/datums/ai/controllers/human_npc.dm +++ b/code/datums/ai/controllers/human_npc.dm @@ -1,31 +1,47 @@ + /datum/ai_controller/human_npc movement_delay = 0.5 SECONDS - ai_movement = /datum/ai_movement/hybrid_pathing - blackboard = list( BB_WEAPON_TYPE = /obj/item/weapon, BB_ARMOR_CLASS = 2, - BB_TARGETTING_DATUM = new /datum/targetting_datum/basic(), BB_PET_TARGETING_DATUM = new /datum/targetting_datum/basic/not_friends(), + BB_HUMAN_NPC_ATTACK_ZONE_COUNTER = 0, // how many times we've hit the same zone + BB_HUMAN_NPC_LAST_ATTACK_ZONE = null, // last zone we attacked + BB_HUMAN_NPC_WEAKPOINT = null, // cached weakpoint zone if we found one + BB_HUMAN_NPC_JUMP_COOLDOWN = 0, // world.time when we can next jump + BB_HUMAN_NPC_FLANK_ANGLE = null, // our claimed flank direction (degrees, 0-359) + BB_HUMAN_NPC_FLANK_TARGET = null, // the turf we're moving toward for flanking + BB_HUMAN_NPC_HARASS_MODE = FALSE, // TRUE when in hit-and-run mode + BB_HUMAN_NPC_HARASS_RETREATING = FALSE,// TRUE when in the back-off phase of harass + BB_HUMAN_NPC_HARASS_COOLDOWN = 0, // world.time before we can dart in again + BB_HUMAN_NPC_JUKE_COOLDOWN = 0, // world.time before we can juke again ) - planning_subtrees = list( /datum/ai_planning_subtree/pet_planning, + /datum/ai_planning_subtree/generic_break_restraints, + /datum/ai_planning_subtree/use_powder, + /datum/ai_planning_subtree/use_bandage, + /datum/ai_planning_subtree/use_throwable, + /datum/ai_planning_subtree/use_healing_drink, + /datum/ai_planning_subtree/throw_grenade, + /datum/ai_planning_subtree/generic_wield, /datum/ai_planning_subtree/generic_resist, /datum/ai_planning_subtree/generic_stand, /datum/ai_planning_subtree/flee_target, - + /datum/ai_planning_subtree/tree_climb, + /datum/ai_planning_subtree/archer_base, + /datum/ai_planning_subtree/ranged_attack_subtree, /datum/ai_planning_subtree/aggro_find_target, - /datum/ai_planning_subtree/basic_melee_attack_subtree, - + /datum/ai_planning_subtree/squad_flank, + /datum/ai_planning_subtree/basic_melee_attack_subtree/human_npc, /datum/ai_planning_subtree/find_weapon, /datum/ai_planning_subtree/equip_item, - + /datum/ai_planning_subtree/retrieve_arrows, + /datum/ai_planning_subtree/loot, ) - idle_behavior = /datum/idle_behavior/idle_random_walk /datum/ai_controller/human_npc/TryPossessPawn(atom/new_pawn) @@ -33,15 +49,15 @@ var/mob/living/living_pawn = new_pawn RegisterSignal(new_pawn, COMSIG_MOB_MOVESPEED_UPDATED, PROC_REF(update_movespeed)) movement_delay = living_pawn.cached_multiplicative_slowdown - + new_pawn.AddComponent(/datum/component/ai_inventory_manager) + new_pawn.AddElement(/datum/element/interrupt_on_damage) + new_pawn.AddComponent(/datum/component/combat_vocalizer) /datum/ai_controller/human_npc/UnpossessPawn(destroy) - UnregisterSignal(pawn, list( COMSIG_MOB_MOVESPEED_UPDATED, )) - - return ..() //Run parent at end + return ..() /datum/ai_controller/human_npc/proc/update_movespeed(mob/living/pawn) SIGNAL_HANDLER @@ -52,5 +68,5 @@ if(!.) return FALSE var/mob/living/living_pawn = pawn - if(living_pawn.pulledby) // to mimick normal human behavior + if(living_pawn.pulledby) return FALSE diff --git a/code/datums/ai/controllers/mirespider.dm b/code/datums/ai/controllers/mirespider.dm new file mode 100644 index 00000000000..a74593bec28 --- /dev/null +++ b/code/datums/ai/controllers/mirespider.dm @@ -0,0 +1,287 @@ +/datum/ai_controller/mirespider + movement_delay = 0.4 SECONDS + + ai_movement = /datum/ai_movement/hybrid_pathing + + blackboard = list( + BB_TARGETTING_DATUM = new /datum/targetting_datum/basic() + ) + + planning_subtrees = list( + /datum/ai_planning_subtree/target_retaliate, + /datum/ai_planning_subtree/aggro_find_target, + + /datum/ai_planning_subtree/simple_self_recovery, + /datum/ai_planning_subtree/find_food, + /datum/ai_planning_subtree/basic_melee_attack_subtree, + /datum/ai_planning_subtree/being_a_minion/mirespider + ) + + idle_behavior = /datum/idle_behavior/idle_random_walk + +/datum/ai_controller/mirespider_lurker + movement_delay = 0.4 SECONDS + + ai_movement = /datum/ai_movement/hybrid_pathing + + blackboard = list( + BB_TARGETTING_DATUM = new /datum/targetting_datum/basic() + ) + + planning_subtrees = list( + /datum/ai_planning_subtree/target_retaliate, + /datum/ai_planning_subtree/aggro_find_target, + /datum/ai_planning_subtree/basic_ranged_attack_subtree/mirespider_lurker, + /datum/ai_planning_subtree/find_cocoon_target, + /datum/ai_planning_subtree/cocoon_target + ) + + idle_behavior = /datum/idle_behavior/idle_random_walk + +/datum/ai_controller/mirespider_paralytic + movement_delay = 0.4 SECONDS + + ai_movement = /datum/ai_movement/hybrid_pathing + + blackboard = list( + BB_TARGETTING_DATUM = new /datum/targetting_datum/basic() + ) + + planning_subtrees = list( + /datum/ai_planning_subtree/target_retaliate, + /datum/ai_planning_subtree/aggro_find_target, + /datum/ai_planning_subtree/basic_ranged_attack_subtree/mirespider_lurker, + /datum/ai_planning_subtree/find_cocoon_target, + /datum/ai_planning_subtree/cocoon_target + ) + + idle_behavior = /datum/idle_behavior/idle_random_walk + +/datum/ai_planning_subtree/being_a_minion/mirespider + /// Blackboard key where we travel a place + location_key = BB_TRAVEL_DESTINATION + /// Who we're following + follow_target = BB_FOLLOW_TARGET + /// What do we do in order to travel + travel_behavior = /datum/ai_behavior/travel_towards/stop_on_arrival + +/datum/ai_planning_subtree/being_a_minion/mirespider/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) + . = ..() + var/turf/travel = controller.blackboard[BB_TRAVEL_DESTINATION] + var/mob/living/simple_animal/hostile/mirespider_lurker/following = controller.blackboard[BB_FOLLOW_TARGET] + var/mob/living/pawn = controller.pawn + + if (travel) // Check if travel is defined + controller.queue_behavior(travel_behavior, BB_TRAVEL_DESTINATION) + return SUBTREE_RETURN_FINISH_PLANNING // end here + + else if (following) // If we're following someone + var/mob/target = following.ai_controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + following.add_follower(pawn) + + // If the follow target has a target, stop following + if (target) + controller.clear_blackboard_key(BB_FOLLOW_TARGET) + + // If too far from the following target, stop following + else if (get_dist(pawn, following) > 12) + controller.clear_blackboard_key(BB_FOLLOW_TARGET) + + // Otherwise, continue following + else + controller.queue_behavior(/datum/ai_behavior/follow_friend/mirespider, BB_FOLLOW_TARGET) + + return SUBTREE_RETURN_FINISH_PLANNING // end here + return // No travel target and no one to follow, being a minion in other ways + +/datum/ai_behavior/follow_friend/mirespider + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM + +/datum/ai_behavior/follow_friend/mirespider/setup(datum/ai_controller/controller, target_key) + . = ..() + var/mob/living/simple_animal/hostile/mirespider_lurker/target = controller.blackboard[target_key] + var/mob/living/simple_animal/hostile/mirespider_lurker/target_target = target.ai_controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + + if (target_target) + return FALSE + + if (QDELETED(target)) + return FALSE + set_movement_target(controller, target) + +/datum/ai_behavior/follow_friend/mirespider/perform(seconds_per_tick, datum/ai_controller/controller, target_key) + var/mob/living/simple_animal/hostile/mirespider_lurker/target = controller.blackboard[target_key] + var/mob/living/simple_animal/hostile/mirespider_lurker/target_target = target.ai_controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + + if (target_target) + return // Stop following if the target has a target + + if (QDELETED(target)) + return + + return + +/datum/ai_planning_subtree/basic_ranged_attack_subtree/mirespider_lurker + ranged_attack_behavior = /datum/ai_behavior/basic_ranged_attack + +/datum/ai_planning_subtree/basic_ranged_attack_subtree/mirespider_lurker/SelectBehaviors(datum/ai_controller/controller, delta_time) + . = ..() + var/atom/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + if(QDELETED(target)) + return + + if(isliving(target)) + var/mob/living/carbon/L = target + if(L) + if(L.stat || L.getBruteLoss() > 500) + controller.set_blackboard_key(BB_BASIC_MOB_COCOON_TARGET, L) + controller.queue_behavior(/datum/ai_behavior/cocoon_target, BB_BASIC_MOB_COCOON_TARGET) + return SUBTREE_RETURN_FINISH_PLANNING + + var/mob/living/simple_animal/hostile/mirespider_lurker/lurker = controller.pawn + if (lurker) + lurker.clear_followers_if_any() + + controller.queue_behavior(ranged_attack_behavior, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETTING_DATUM, BB_BASIC_MOB_CURRENT_TARGET_HIDING_LOCATION) + return SUBTREE_RETURN_FINISH_PLANNING + +/datum/ai_behavior/cocoon_target + behavior_flags = AI_BEHAVIOR_REQUIRE_REACH | AI_BEHAVIOR_REQUIRE_MOVEMENT + action_cooldown = 1 SECONDS + +/datum/ai_behavior/cocoon_target/setup(datum/ai_controller/controller, target_key) + . = ..() + var/mob/living/target = controller.blackboard[target_key] + if (!target || QDELETED(target) || !isliving(target)) + return FALSE + + set_movement_target(controller, (target)) + +/datum/ai_behavior/cocoon_target/perform(seconds_per_tick, datum/ai_controller/controller, target_key) + . = ..() + var/mob/living/simple_animal/pawn = controller.pawn + var/mob/living/target = controller.blackboard[target_key] + if (!target || QDELETED(target) || !isliving(target)) + finish_action(controller, FALSE, target_key) + return + + if (istype(target.loc, /obj/structure/spider/cocoon)) + finish_action(controller, TRUE, target_key) + return + + if (target.stat) + if (do_after(pawn, 5 SECONDS, FALSE, target)) + if (istype(target.loc, /obj/structure/spider/cocoon)) + finish_action(controller, TRUE, target_key) + return + var/turf/T = get_turf(target) + var/cocoon = new /obj/structure/spider/cocoon(T) + target.forceMove(cocoon) + // Very gentle healing effect that restores a lot of bloodloss. Allows the target to break out later. + target.apply_status_effect(/datum/status_effect/buff/healing/spider_cocoon, 0.25) + finish_action(controller, TRUE, target_key) + +/datum/ai_behavior/cocoon_target/finish_action(datum/ai_controller/controller, succeeded, target_key) + . = ..() + if(!succeeded) + controller.clear_blackboard_key(target_key) + controller.clear_blackboard_key(target_key) + +/datum/ai_planning_subtree/cocoon_target + var/datum/ai_behavior/cocoon_target/behavior = /datum/ai_behavior/cocoon_target + +/datum/ai_planning_subtree/cocoon_target/SelectBehaviors(datum/ai_controller/controller, delta_time) + . = ..() + var/obj/item/target = controller.blackboard[BB_BASIC_MOB_COCOON_TARGET] + if(QDELETED(target)) + controller.clear_blackboard_key(BB_BASIC_MOB_COCOON_TARGET) + return + var/mob/living/pawn = controller.pawn + if(pawn.doing()) + return + if(!istype(target, /mob/living/carbon)) + behavior = /datum/ai_behavior/cocoon_target + + controller.queue_behavior(behavior, BB_BASIC_MOB_COCOON_TARGET) + return SUBTREE_RETURN_FINISH_PLANNING + +/datum/ai_planning_subtree/find_cocoon_target + var/vision_range = 6 + var/datum/ai_behavior/find_and_set/cocoon_target/behavior = /datum/ai_behavior/find_and_set/cocoon_target + var/cocoon_target_key = BB_BASIC_MOB_COCOON_TARGET + +/datum/ai_planning_subtree/find_cocoon_target/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) + . = ..() + + var/atom/target = controller.blackboard[cocoon_target_key] + if(!QDELETED(target)) + // Busy with something + return + + controller.queue_behavior(behavior, cocoon_target_key, controller.blackboard[BB_BASIC_FOODS], vision_range) + +/datum/ai_behavior/find_and_set/cocoon_target + action_cooldown = 20 SECONDS + +/datum/ai_behavior/find_and_set/cocoon_target/search_tactic(datum/ai_controller/controller, locate_paths, search_range) + var/list/found = list() + for(var/mob/living/carbon/mob in oview(search_range, controller.pawn)) + var/obj/structure/spider/cocoon/cocoon = mob.loc + if(istype(cocoon, /obj/structure/spider/cocoon)) + continue + if(mob.stat == DEAD || mob.stat == CONSCIOUS) + continue + found |= mob + if(!length(found)) + return null + return pick(found) + +/atom/movable/screen/alert/status_effect/buff/healing/spider_cocoon + name = "Spider loogies" + desc = "Arachnid weave is stitching some of my wounds up slowly." + icon_state = "buff" + +#define COCOON_FILTER "cocoon_glow" + +/datum/status_effect/buff/healing/spider_cocoon + id = "healing_spider" + alert_type = /atom/movable/screen/alert/status_effect/buff/healing/spider_cocoon + duration = 1800 SECONDS + examine_text = "SUBJECTPRONOUN is covered in spider silk... eww!" + healing_on_tick = 1 + outline_colour = "#4e4c4c00" + var/blood_healing_on_tick = 20 + +/datum/status_effect/buff/healing/spider_cocoon/on_apply() + . = ..() + //The hardier you are, the more likely you are to recover from grievous wounds. + var/stat_bonus = 0 + stat_bonus += ((owner.STACON - 10 ) * 0.05) + stat_bonus += ((owner.STASTR - 10 ) * 0.05) + stat_bonus += ((owner.STAEND - 10 ) * 0.05) + if(stat_bonus > 0) + healing_on_tick += stat_bonus + blood_healing_on_tick += (stat_bonus * 10) + var/filter = owner.get_filter(COCOON_FILTER) + if (!filter) + owner.add_filter(COCOON_FILTER, 2, list("type" = "outline", "color" = outline_colour, "alpha" = 60, "size" = 1)) + return TRUE + +/datum/status_effect/buff/healing/spider_cocoon/tick() + var/obj/effect/temp_visual/heal/H = new /obj/effect/temp_visual/heal_rogue(get_turf(owner)) + H.color = "#4e4c4c00" + var/list/wCount = owner.get_wounds() + if(owner.blood_volume < BLOOD_VOLUME_NORMAL) + //Keeps the user alive + owner.blood_volume = min(owner.blood_volume+blood_healing_on_tick, BLOOD_VOLUME_NORMAL) + if(wCount.len > 0) + owner.heal_wounds(healing_on_tick) + owner.update_damage_overlays() + owner.adjustBruteLoss(-healing_on_tick, 0) + owner.adjustFireLoss(-healing_on_tick, 0) + owner.adjustOxyLoss(-healing_on_tick * 5, 0) + owner.adjustToxLoss(-healing_on_tick, 0) + owner.adjustOrganLoss(ORGAN_SLOT_BRAIN, -healing_on_tick) + owner.adjustCloneLoss(-healing_on_tick, 0) + +#undef COCOON_FILTER diff --git a/code/datums/ai/subtrees/__bow_base.dm b/code/datums/ai/subtrees/__bow_base.dm new file mode 100644 index 00000000000..606e9ff7eb6 --- /dev/null +++ b/code/datums/ai/subtrees/__bow_base.dm @@ -0,0 +1,38 @@ +/datum/ai_planning_subtree/archer_base/proc/validate_archer_equipment(datum/ai_controller/controller) + if(world.time < controller.blackboard[BB_ARCHER_NPC_EQUIPMENT_CACHE_EXPIRY]) + var/obj/item/gun/ballistic/revolver/grenadelauncher/bow/cached_bow = controller.blackboard[BB_ARCHER_NPC_BOW] + var/obj/item/ammo_holder/quiver/cached_quiver = controller.blackboard[BB_ARCHER_NPC_QUIVER] + if(QDELETED(cached_bow) || QDELETED(cached_quiver)) + _clear_equipment_cache(controller) + return FALSE + return TRUE + + _clear_equipment_cache(controller) + + var/datum/component/ai_inventory_manager/inv = controller.get_inventory() + if(!inv) + return FALSE + + var/mob/living/living_pawn = controller.pawn + var/obj/item/gun/ballistic/revolver/grenadelauncher/bow = inv.get_item(AI_ITEM_GUN) + if(!bow) + if(istype(living_pawn.get_active_held_item(), /obj/item/gun/ballistic/revolver/grenadelauncher)) + bow = living_pawn.get_active_held_item() + else if(istype(living_pawn.get_inactive_held_item(), /obj/item/gun/ballistic/revolver/grenadelauncher)) + bow = living_pawn.get_inactive_held_item() + if(!bow) + return FALSE + + var/obj/item/ammo_holder/quiver/quiver = inv.get_item(AI_ITEM_QUIVER) + if(!quiver?.ammo_list.len) + return FALSE + + controller.set_blackboard_key(BB_ARCHER_NPC_BOW, bow) + controller.set_blackboard_key(BB_ARCHER_NPC_QUIVER, quiver) + controller.set_blackboard_key(BB_ARCHER_NPC_EQUIPMENT_CACHE_EXPIRY, world.time + ARCHER_NPC_EQUIPMENT_CACHE_TIME) + return TRUE + +/datum/ai_planning_subtree/archer_base/proc/_clear_equipment_cache(datum/ai_controller/controller) + controller.clear_blackboard_key(BB_ARCHER_NPC_BOW) + controller.clear_blackboard_key(BB_ARCHER_NPC_QUIVER) + controller.clear_blackboard_key(BB_ARCHER_NPC_EQUIPMENT_CACHE_EXPIRY) diff --git a/code/datums/ai/subtrees/be_a_minion.dm b/code/datums/ai/subtrees/be_a_minion.dm new file mode 100644 index 00000000000..9596bc4df67 --- /dev/null +++ b/code/datums/ai/subtrees/be_a_minion.dm @@ -0,0 +1,41 @@ +/// Obey your summoner (or equivalent) +/datum/ai_planning_subtree/being_a_minion + /// Blackboard key where we travel a place + var/location_key = BB_TRAVEL_DESTINATION + /// Who we're following + var/follow_target = BB_FOLLOW_TARGET + /// What do we do in order to travel + var/travel_behavior = /datum/ai_behavior/travel_towards/stop_on_arrival +/datum/ai_planning_subtree/being_a_minion/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) + . = ..() + var/turf/travel = controller.blackboard[location_key] + var/mob/following = controller.blackboard[follow_target] + var/mob/living/pawn = controller.pawn + + if(travel) + controller.queue_behavior(travel_behavior, location_key) + return SUBTREE_RETURN_FINISH_PLANNING //end here + else if(following) + if(get_dist(pawn, following) > 12) //If further than 12 then you've lost that friendly target + controller.clear_blackboard_key(BB_FOLLOW_TARGET) + else + controller.queue_behavior(/datum/ai_behavior/follow_friend, follow_target) + return SUBTREE_RETURN_FINISH_PLANNING //end here + return //no travel target and no one to follow. being a minion in other ways +/// Follow the target +/datum/ai_behavior/follow_friend + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM +/datum/ai_behavior/follow_friend/setup(datum/ai_controller/controller, target_key) + . = ..() + var/atom/target = controller.blackboard[target_key] + + if (QDELETED(target)) + return FALSE + set_movement_target(controller, target) +/datum/ai_behavior/follow_friend/perform(seconds_per_tick, datum/ai_controller/controller, target_key) + var/mob/target = controller.blackboard[target_key] + + if (QDELETED(target)) + return + + return diff --git a/code/datums/ai/subtrees/bow_usage.dm b/code/datums/ai/subtrees/bow_usage.dm new file mode 100644 index 00000000000..17e0cde986a --- /dev/null +++ b/code/datums/ai/subtrees/bow_usage.dm @@ -0,0 +1,169 @@ +/datum/ai_planning_subtree/ranged_attack_subtree + parent_type = /datum/ai_planning_subtree/archer_base + +/datum/ai_planning_subtree/ranged_attack_subtree/SelectBehaviors(datum/ai_controller/controller, delta_time) + if(!validate_archer_equipment(controller)) + return + var/mob/living/carbon/human/pawn = controller.pawn + var/atom/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + if(!target || !isliving(target)) + var/obj/item/stashed = controller.blackboard[BB_ARCHER_NPC_STASHED_WEAPON] + if(stashed && !QDELETED(stashed)) + if(!pawn.get_active_held_item()) + pawn.dropItemToGround(stashed, TRUE, TRUE) + pawn.put_in_active_hand(stashed) + else if(!pawn.get_inactive_held_item()) + pawn.dropItemToGround(stashed, TRUE, TRUE) + pawn.put_in_inactive_hand(stashed) + controller.clear_blackboard_key(BB_ARCHER_NPC_STASHED_WEAPON) + return + + var/obj/item/ammo_holder/quiver/Q = controller.blackboard[BB_ARCHER_NPC_QUIVER] + if(!Q.ammo_list.len) + return + + if(get_dist(pawn, target) < ARCHER_NPC_MIN_RANGE) + var/obj/item/stashed = controller.blackboard[BB_ARCHER_NPC_STASHED_WEAPON] + if(stashed && !QDELETED(stashed)) + if(!pawn.get_active_held_item()) + pawn.dropItemToGround(stashed, TRUE, TRUE) + pawn.put_in_active_hand(stashed) + else if(!pawn.get_inactive_held_item()) + pawn.dropItemToGround(stashed, TRUE, TRUE) + pawn.put_in_inactive_hand(stashed) + controller.clear_blackboard_key(BB_ARCHER_NPC_STASHED_WEAPON) + return + + controller.queue_behavior(/datum/ai_behavior/ranged_attack_bow, BB_BASIC_MOB_CURRENT_TARGET) + return SUBTREE_RETURN_FINISH_PLANNING + +/datum/ai_behavior/ranged_attack_bow + behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION | AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_MOVE_AND_PERFORM + action_cooldown = 0.2 SECONDS + required_distance = ARCHER_NPC_MIN_RANGE + 4 + +/datum/ai_behavior/ranged_attack_bow/setup(datum/ai_controller/controller, target_key) + . = ..() + if(!.) + return FALSE + var/mob/living/carbon/human/pawn = controller.pawn + var/atom/target = controller.blackboard[target_key] + if(!target) + return FALSE + + // Stash held melee weapon if needed so both hands are free for the bow + for(var/obj/item/held in pawn.get_active_held_items()) + if(istype(held, /obj/item/gun/ballistic/revolver/grenadelauncher/bow)) + continue + if(!held) + continue + var/stashed = FALSE + for(var/slot in list(ITEM_SLOT_BACK, ITEM_SLOT_HIP, ITEM_SLOT_BELT_L, ITEM_SLOT_BACK_L, ITEM_SLOT_BACK_R, ITEM_SLOT_BELT_R)) + if(!pawn.get_item_by_slot(slot)) + if(pawn.equip_to_slot_if_possible(held, slot, disable_warning = TRUE)) + controller.set_blackboard_key(BB_ARCHER_NPC_STASHED_WEAPON, held) + stashed = TRUE + break + if(!stashed) + controller.clear_blackboard_key(BB_ARCHER_NPC_QUIVER) //this is weird you might say? but it saves a memory slot since it cannot execute a bow shot without a quiver causing it to go on cooldown for 40 seconds. + return FALSE + + var/obj/item/gun/ballistic/revolver/grenadelauncher/bow/bow = null + for(var/obj/item/held in pawn.get_active_held_items()) + if(istype(held, /obj/item/gun/ballistic/revolver/grenadelauncher/bow)) + bow = held + break + if(!bow) + for(var/obj/item/worn in pawn.get_equipped_items()) + if(istype(worn, /obj/item/gun/ballistic/revolver/grenadelauncher/bow)) + pawn.put_in_active_hand(worn) + bow = worn + break + if(!bow) + return FALSE + + if(!bow.chambered) + var/ammo_check = bow.magazine.ammo_type + for(var/obj/item/ammo_holder/quiver/Q in pawn.get_equipped_items()) + if(!Q.ammo_list.len) + continue + for(var/obj/item/ammo_casing/arrow in Q.ammo_list) + if(ispath(arrow.type, ammo_check)) + Q.ammo_list -= arrow + arrow.forceMove(bow) + // Mirror what attackby does for loading + bow.attackby(arrow, pawn, null) + break + break + + if(!bow.chambered) + return FALSE + + // For crossbows, ensure cocked + if(istype(bow, /obj/item/gun/ballistic/revolver/grenadelauncher/crossbow)) + var/obj/item/gun/ballistic/revolver/grenadelauncher/crossbow/xbow = bow + if(!xbow.cocked) + xbow.cocked = TRUE + xbow.update_appearance(UPDATE_ICON_STATE) + + set_movement_target(controller, target) + SEND_SIGNAL(controller.pawn, COMSIG_COMBAT_TARGET_SET, TRUE) + return TRUE + +/datum/ai_behavior/ranged_attack_bow/perform(delta_time, datum/ai_controller/controller, target_key) + var/mob/living/carbon/human/pawn = controller.pawn + var/atom/target = controller.blackboard[target_key] + + + if(!target || (ismob(target) && target:stat == DEAD)) + finish_action(controller, FALSE, target_key) + return + + // Break off if target closed to melee range + if(get_dist(pawn, target) < ARCHER_NPC_MIN_RANGE) + finish_action(controller, FALSE, target_key) + return + + if(!can_see(pawn, target, 11)) + finish_action(controller, FALSE, target_key) + return + + var/obj/item/gun/ballistic/revolver/grenadelauncher/bow/bow = null + for(var/obj/item/held in pawn.get_active_held_items()) + if(istype(held, /obj/item/gun/ballistic/revolver/grenadelauncher/bow)) + bow = held + break + if(!bow || !bow.chambered) + finish_action(controller, FALSE, target_key) + return + + var/chargetime = ARCHER_NPC_SIMULATED_CHARGETIME + if(pawn.used_intent && pawn.used_intent.chargetime) + chargetime = pawn.used_intent.get_chargetime() + + if(!do_after(pawn, min(chargetime, 1 SECONDS), pawn)) + finish_action(controller, FALSE, target_key) + return + + pawn.face_atom(target) + controller.ai_interact(target, TRUE, TRUE) + + finish_action(controller, TRUE, target_key) + +/datum/ai_behavior/ranged_attack_bow/finish_action(datum/ai_controller/controller, succeeded, target_key) + . = ..() + var/mob/living/carbon/human/pawn = controller.pawn + var/obj/item/ammo_holder/quiver/Q = controller.blackboard[BB_ARCHER_NPC_QUIVER] + + if(!succeeded || !length(Q.ammo_list)) + controller.clear_blackboard_key(target_key) + // Re-equip stashed melee weapon + var/obj/item/stashed = controller.blackboard[BB_ARCHER_NPC_STASHED_WEAPON] + if(stashed && !QDELETED(stashed)) + if(!pawn.get_active_held_item()) + pawn.dropItemToGround(stashed, TRUE, TRUE) + pawn.put_in_active_hand(stashed) + else if(!pawn.get_inactive_held_item()) + pawn.dropItemToGround(stashed, TRUE, TRUE) + pawn.put_in_inactive_hand(stashed) + controller.clear_blackboard_key(BB_ARCHER_NPC_STASHED_WEAPON) diff --git a/code/datums/ai/subtrees/climb_tree.dm b/code/datums/ai/subtrees/climb_tree.dm new file mode 100644 index 00000000000..95370200e12 --- /dev/null +++ b/code/datums/ai/subtrees/climb_tree.dm @@ -0,0 +1,52 @@ +/datum/ai_planning_subtree/tree_climb + +/datum/ai_planning_subtree/tree_climb/SelectBehaviors(datum/ai_controller/controller, delta_time) + . = ..() + var/mob/living/carbon/human/pawn = controller.pawn + var/atom/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + if(!target) + return + var/turf/my_turf = get_turf(pawn) + var/turf/their_turf = get_turf(target) + // Target must be directly above us (z+1) on a transparent turf, cardinal distance 1 + if(!their_turf || my_turf.z >= their_turf.z) + return + if(my_turf.Distance3D(their_turf, pawn) != 1) + return + if(!istransparentturf(their_turf)) + return + // There must be a branch on their turf to climb + var/obj/structure/flora/newbranch/the_branch = locate() in their_turf + if(!the_branch) + return + controller.queue_behavior(/datum/ai_behavior/human_npc_climb_tree, BB_BASIC_MOB_CURRENT_TARGET) + return SUBTREE_RETURN_FINISH_PLANNING + +/datum/ai_behavior/human_npc_climb_tree + action_cooldown = 1 SECONDS + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT + +/datum/ai_behavior/human_npc_climb_tree/perform(delta_time, datum/ai_controller/controller, target_key) + var/mob/living/carbon/human/pawn = controller.pawn + var/atom/target = controller.blackboard[target_key] + if(!target) + finish_action(controller, FALSE, target_key) + return + var/turf/their_turf = get_turf(target) + var/turf/my_turf = get_turf(pawn) + // Revalidate - target may have moved + if(!their_turf || my_turf.z >= their_turf.z || !istransparentturf(their_turf)) + finish_action(controller, FALSE, target_key) + return + var/obj/structure/flora/newbranch/the_branch = locate() in their_turf + if(!the_branch) + finish_action(controller, FALSE, target_key) + return + // Find the tree from the branch (same logic as process_ai) + var/obj/structure/flora/newtree/the_tree = locate() in get_step_multiz(the_branch, REVERSE_DIR(the_branch.dir)|DOWN) + if(!the_tree) + finish_action(controller, FALSE, target_key) + return + the_tree.attack_hand(pawn) + finish_action(controller, TRUE, target_key) + diff --git a/code/datums/ai/subtrees/flank.dm b/code/datums/ai/subtrees/flank.dm new file mode 100644 index 00000000000..05f4908ff83 --- /dev/null +++ b/code/datums/ai/subtrees/flank.dm @@ -0,0 +1,135 @@ +#define FLANK_RADIUS 3 // tiles away from target to orbit +#define FLANK_MIN_SEPARATION 60 // degrees between us and nearest ally +#define FLANK_ENGAGE_DIST 2 // tiles - "close enough" to our flank spot +#define FLANK_ATTACK_CHANCE 25 // % chance to commit a real attack while flanking +#define FLANK_RECHECK_INTERVAL (3 SECONDS) + +/datum/ai_planning_subtree/squad_flank + +/datum/ai_planning_subtree/squad_flank/SelectBehaviors(datum/ai_controller/controller, delta_time) + . = ..() + var/mob/living/carbon/human/pawn = controller.pawn + var/mob/living/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + if(!target || QDELETED(target)) + controller.clear_blackboard_key(BB_HUMAN_NPC_FLANK_ANGLE) + controller.clear_blackboard_key(BB_HUMAN_NPC_FLANK_TARGET) + return + + var/list/ally_angles = list() + var/turf/target_turf = get_turf(target) + for(var/mob/living/carbon/human/ally in view(10, pawn)) + if(ally == pawn) + continue + if(!faction_check(pawn.faction, ally.faction)) + continue + var/datum/ai_controller/human_npc/ally_ctrl = ally.ai_controller + if(!ally_ctrl) + continue + var/mob/living/ally_target = ally_ctrl.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + if(ally_target != target) + continue + // What angle is this ally at from the target? + var/turf/ally_turf = get_turf(ally) + if(!ally_turf || !target_turf) + continue + var/dx = ally_turf.x - target_turf.x + var/dy = ally_turf.y - target_turf.y + var/angle = round(arctan(dy, dx) + 360) % 360 + ally_angles += angle + + // If no allies are fighting this target, no need to flank + if(!length(ally_angles)) + controller.clear_blackboard_key(BB_HUMAN_NPC_FLANK_ANGLE) + controller.clear_blackboard_key(BB_HUMAN_NPC_FLANK_TARGET) + return + + // Sort angles and find the largest gap + ally_angles = sortList(ally_angles) + var/largest_gap = 0 + var/gap_start_angle = 0 + for(var/i in 1 to length(ally_angles)) + var/next_i = (i % length(ally_angles)) + 1 + var/gap = (ally_angles[next_i] - ally_angles[i] + 360) % 360 + if(gap > largest_gap) + largest_gap = gap + gap_start_angle = ally_angles[i] + + // Not enough angular separation to bother flanking + if(largest_gap < FLANK_MIN_SEPARATION) + controller.clear_blackboard_key(BB_HUMAN_NPC_FLANK_ANGLE) + controller.clear_blackboard_key(BB_HUMAN_NPC_FLANK_TARGET) + return + + // Our ideal angle is the midpoint of the largest gap + var/my_angle = (gap_start_angle + round(largest_gap / 2)) % 360 + + var/cached_angle = controller.blackboard[BB_HUMAN_NPC_FLANK_ANGLE] + var/turf/flank_turf = controller.blackboard[BB_HUMAN_NPC_FLANK_TARGET] + + // Recalculate if our angle has shifted more than 30 degrees or we have no turf + if(isnull(cached_angle) || abs(cached_angle - my_angle) > 30 || QDELETED(flank_turf)) + var/fx = round(target_turf.x + FLANK_RADIUS * cos(my_angle)) + var/fy = round(target_turf.y + FLANK_RADIUS * sin(my_angle)) + flank_turf = locate(clamp(fx, 1, world.maxx), clamp(fy, 1, world.maxy), target_turf.z) + var/search_attempts = 0 + while(flank_turf && (!flank_turf.can_traverse_safely(pawn) || flank_turf.density) && search_attempts < FLANK_RADIUS) + flank_turf = get_step_towards(flank_turf, target_turf) + search_attempts++ + if(!flank_turf || !flank_turf.can_traverse_safely(pawn)) + return + controller.set_blackboard_key(BB_HUMAN_NPC_FLANK_ANGLE, my_angle) + controller.set_blackboard_key(BB_HUMAN_NPC_FLANK_TARGET, flank_turf) + + if(get_dist(pawn, flank_turf) <= FLANK_ENGAGE_DIST) + // We're in position. Occasionally fire an attack, otherwise just hold. + if(prob(FLANK_ATTACK_CHANCE)) + controller.clear_blackboard_key(BB_HUMAN_NPC_FLANK_TARGET) + return + // Hold position, face target, look threatening + pawn.face_atom(target) + return SUBTREE_RETURN_FINISH_PLANNING + controller.queue_behavior(/datum/ai_behavior/human_npc_move_to_flank, BB_HUMAN_NPC_FLANK_TARGET, BB_BASIC_MOB_CURRENT_TARGET) + return SUBTREE_RETURN_FINISH_PLANNING + +/datum/ai_behavior/human_npc_move_to_flank + action_cooldown = 0.2 SECONDS + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + +/datum/ai_behavior/human_npc_move_to_flank/setup(datum/ai_controller/controller, flank_turf_key, target_key) + . = ..() + var/turf/flank_turf = controller.blackboard[flank_turf_key] + if(!flank_turf || QDELETED(flank_turf)) + return FALSE + set_movement_target(controller, flank_turf) + +/datum/ai_behavior/human_npc_move_to_flank/perform(delta_time, datum/ai_controller/controller, flank_turf_key, target_key) + var/mob/living/carbon/human/pawn = controller.pawn + var/turf/flank_turf = controller.blackboard[flank_turf_key] + var/mob/living/target = controller.blackboard[target_key] + + if(!flank_turf || QDELETED(flank_turf)) + finish_action(controller, FALSE) + return + + // If the target moved a lot, the flank turf may no longer make sense - let subtree recalculate + if(target && get_dist(get_turf(target), flank_turf) > FLANK_RADIUS + 3) + controller.clear_blackboard_key(BB_HUMAN_NPC_FLANK_ANGLE) + controller.clear_blackboard_key(flank_turf_key) + finish_action(controller, FALSE) + return + + // Face the target while moving so we look alert + if(target) + pawn.face_atom(target) + + if(get_dist(pawn, flank_turf) <= FLANK_ENGAGE_DIST) + finish_action(controller, TRUE) + return + + finish_action(controller, FALSE) + +#undef FLANK_RADIUS +#undef FLANK_MIN_SEPARATION +#undef FLANK_ENGAGE_DIST +#undef FLANK_ATTACK_CHANCE +#undef FLANK_RECHECK_INTERVAL diff --git a/code/datums/ai/subtrees/generic_restraint.dm b/code/datums/ai/subtrees/generic_restraint.dm new file mode 100644 index 00000000000..2770fba75c6 --- /dev/null +++ b/code/datums/ai/subtrees/generic_restraint.dm @@ -0,0 +1,28 @@ +/datum/ai_planning_subtree/generic_break_restraints/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) + var/mob/living/carbon/living_pawn = controller.pawn + if(!isliving(living_pawn)) + return + + if(!living_pawn.handcuffed && !living_pawn.legcuffed) + return + + if(SPT_PROB(50, seconds_per_tick)) + controller.queue_behavior(/datum/ai_behavior/break_restraints) + return SUBTREE_RETURN_FINISH_PLANNING + +/datum/ai_behavior/break_restraints + behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + action_cooldown = 30 SECONDS + +/datum/ai_behavior/break_restraints/perform(seconds_per_tick, datum/ai_controller/controller) + var/mob/living/carbon/living_pawn = controller.pawn + if(!living_pawn.handcuffed && !living_pawn.legcuffed) + finish_action(controller, FALSE) + return + + if(living_pawn.handcuffed) + living_pawn.cuff_resist(living_pawn.handcuffed, instant = TRUE) + else if(living_pawn.legcuffed) + living_pawn.cuff_resist(living_pawn.legcuffed, instant = TRUE) + + finish_action(controller, TRUE) diff --git a/code/datums/ai/subtrees/generic_wield.dm b/code/datums/ai/subtrees/generic_wield.dm new file mode 100644 index 00000000000..068b372c9f4 --- /dev/null +++ b/code/datums/ai/subtrees/generic_wield.dm @@ -0,0 +1,28 @@ +/datum/ai_planning_subtree/generic_wield/SelectBehaviors(datum/ai_controller/controller, seconds_per_tick) + var/mob/living/living_pawn = controller.pawn + var/obj/item/active = living_pawn.get_active_held_item() + var/obj/item/inactive = living_pawn.get_inactive_held_item() + if(active && inactive) + return + var/obj/item/unwielded_twohander = null + if(active?.GetComponent(/datum/component/two_handed) && !HAS_TRAIT(active, TRAIT_WIELDED)) + unwielded_twohander = active + else if(inactive?.GetComponent(/datum/component/two_handed) && !HAS_TRAIT(inactive, TRAIT_WIELDED)) + unwielded_twohander = inactive + if(unwielded_twohander) + controller.queue_behavior(/datum/ai_behavior/wield_weapon) + return SUBTREE_RETURN_FINISH_PLANNING + +/datum/ai_behavior/wield_weapon/perform(seconds_per_tick, datum/ai_controller/controller) + var/mob/living/living_pawn = controller.pawn + var/obj/item/active = living_pawn.get_active_held_item() + var/obj/item/inactive = living_pawn.get_inactive_held_item() + var/obj/item/to_wield = null + if(active?.GetComponent(/datum/component/two_handed)&& !HAS_TRAIT(active, TRAIT_WIELDED)) + to_wield = active + else if(inactive?.GetComponent(/datum/component/two_handed) && !HAS_TRAIT(inactive, TRAIT_WIELDED)) + to_wield = inactive + if(to_wield) + var/datum/component/two_handed/twohanded = to_wield.GetComponent(/datum/component/two_handed) + twohanded.wield(living_pawn) + finish_action(controller, TRUE) diff --git a/code/datums/ai/subtrees/harass.dm b/code/datums/ai/subtrees/harass.dm new file mode 100644 index 00000000000..e2263079847 --- /dev/null +++ b/code/datums/ai/subtrees/harass.dm @@ -0,0 +1,164 @@ +#define HARASS_HEALTH_THRESHOLD 0.65 // below 65% max health = consider harassing +#define HARASS_STAMINA_THRESHOLD 55 // above this stamina damage = consider harassing +#define HARASS_RETREAT_DIST 4 // tiles to back off after an attack +#define HARASS_BASE_COOLDOWN (4 SECONDS) // base pause between dart-ins +#define HARASS_STAMINA_COOLDOWN_SCALE 0.05 // extra seconds per point of stamina over threshold + +/datum/ai_planning_subtree/wounded_harass + +/datum/ai_planning_subtree/wounded_harass/SelectBehaviors(datum/ai_controller/controller, delta_time) + . = ..() + var/mob/living/carbon/human/pawn = controller.pawn + var/mob/living/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + if(!target || QDELETED(target)) + controller.set_blackboard_key(BB_HUMAN_NPC_HARASS_MODE, FALSE) + controller.set_blackboard_key(BB_HUMAN_NPC_HARASS_RETREATING, FALSE) + return + + var/health_frac = pawn.health / pawn.maxHealth //idk if this does anything for humans + // Stamina threshold scales down as health drops - the more hurt we are the easier tiredness triggers harass + var/effective_stamina_thresh = HARASS_STAMINA_THRESHOLD * health_frac + var/should_harass = (health_frac < HARASS_HEALTH_THRESHOLD) || (pawn.stamina > effective_stamina_thresh) + + var/currently_harassing = controller.blackboard[BB_HUMAN_NPC_HARASS_MODE] + + if(!should_harass && !currently_harassing) + return // Healthy and not tired, normal combat + + if(!should_harass && currently_harassing) + // ewxit harass mode once we finish a retreat + if(!controller.blackboard[BB_HUMAN_NPC_HARASS_RETREATING]) + controller.set_blackboard_key(BB_HUMAN_NPC_HARASS_MODE, FALSE) + return + // Still retreating from last dart, finish it out + controller.queue_behavior(/datum/ai_behavior/human_npc_harass_retreat, BB_BASIC_MOB_CURRENT_TARGET) + return SUBTREE_RETURN_FINISH_PLANNING + + controller.set_blackboard_key(BB_HUMAN_NPC_HARASS_MODE, TRUE) + + if(controller.blackboard[BB_HUMAN_NPC_HARASS_RETREATING]) + if(world.time < controller.blackboard[BB_HUMAN_NPC_HARASS_COOLDOWN]) + //back off and wait + controller.queue_behavior(/datum/ai_behavior/human_npc_harass_retreat, BB_BASIC_MOB_CURRENT_TARGET) + return SUBTREE_RETURN_FINISH_PLANNING + controller.set_blackboard_key(BB_HUMAN_NPC_HARASS_RETREATING, FALSE) + return SUBTREE_RETURN_FINISH_PLANNING // re-plan next tick into DART IN + + // If we're adjacent, fire the attack then immediately start retreating + if(pawn.Adjacent(target)) + controller.queue_behavior(/datum/ai_behavior/human_npc_harass_strike, BB_BASIC_MOB_CURRENT_TARGET, BB_TARGETTING_DATUM) + return SUBTREE_RETURN_FINISH_PLANNING + + // Not adjacent - move in + controller.queue_behavior(/datum/ai_behavior/human_npc_harass_dart_in, BB_BASIC_MOB_CURRENT_TARGET) + return SUBTREE_RETURN_FINISH_PLANNING + +/datum/ai_behavior/human_npc_harass_dart_in + action_cooldown = 0.2 SECONDS + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + +/datum/ai_behavior/human_npc_harass_dart_in/setup(datum/ai_controller/controller, target_key) + . = ..() + var/atom/target = controller.blackboard[target_key] + if(!target || QDELETED(target)) + return FALSE + set_movement_target(controller, target) + +/datum/ai_behavior/human_npc_harass_dart_in/perform(delta_time, datum/ai_controller/controller, target_key) + var/mob/living/carbon/human/pawn = controller.pawn + var/mob/living/target = controller.blackboard[target_key] + if(!target || QDELETED(target)) + finish_action(controller, FALSE) + return + pawn.face_atom(target) + if(pawn.Adjacent(target)) + finish_action(controller, TRUE) // adjacent, subtree will now queue the strike + return + + +/datum/ai_behavior/human_npc_harass_dart_in/finish_action(datum/ai_controller/controller, succeeded, target_key) + . = ..() + +/datum/ai_behavior/human_npc_harass_strike + action_cooldown = 0.2 SECONDS + behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + +/datum/ai_behavior/human_npc_harass_strike/perform(delta_time, datum/ai_controller/controller, target_key, targetting_datum_key) + var/mob/living/carbon/human/pawn = controller.pawn + var/mob/living/target = controller.blackboard[target_key] + var/datum/targetting_datum/td = controller.blackboard[targetting_datum_key] + + if(!target || QDELETED(target) || !td.can_attack(pawn, target)) + finish_action(controller, FALSE) + return + + if(pawn.next_move > world.time) // not ready to swing yet + finish_action(controller, FALSE) + return + + pawn.face_atom(target) + + // Pick an intent - same logic as smart melee + var/list/possible_intents = list() + for(var/datum/intent/intent as anything in pawn.possible_a_intents) + if(istype(intent, /datum/intent/unarmed/help) || istype(intent, /datum/intent/unarmed/shove) || istype(intent, /datum/intent/unarmed/grab)) + continue + possible_intents |= intent + if(length(possible_intents)) + pawn.a_intent = pick(possible_intents) + pawn.used_intent = pawn.a_intent + + if(!pawn.CanReach(target)) + finish_action(controller, FALSE) + return + + controller.ai_interact(target, TRUE, TRUE) + if(pawn.next_click < world.time) + pawn.next_click = world.time + (pawn.used_intent?.clickcd * ( 1 + rand(0.2, 0.4))) + SEND_SIGNAL(pawn, COMSIG_MOB_BREAK_SNEAK) + + // Cooldown scales up with stamina damage and scales down with health - the more gassed/hurt, the longer we hide + var/health_frac = pawn.health / pawn.maxHealth + var/stamina_over = max(0, pawn.stamina - HARASS_STAMINA_THRESHOLD) + var/cooldown = HARASS_BASE_COOLDOWN + cooldown += stamina_over * HARASS_STAMINA_COOLDOWN_SCALE SECONDS + cooldown *= lerp(1.5, 1.0, health_frac) // up to 50% longer cooldown at 0 health + controller.set_blackboard_key(BB_HUMAN_NPC_HARASS_RETREATING, TRUE) + controller.set_blackboard_key(BB_HUMAN_NPC_HARASS_COOLDOWN, world.time + cooldown) + finish_action(controller, TRUE) + +/datum/ai_behavior/human_npc_harass_retreat + action_cooldown = 0.2 SECONDS + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + +/datum/ai_behavior/human_npc_harass_retreat/setup(datum/ai_controller/controller, target_key) + . = ..() + var/mob/living/carbon/human/pawn = controller.pawn + var/mob/living/target = controller.blackboard[target_key] + if(!target || QDELETED(target)) + return FALSE + var/turf/retreat_turf = get_turf(pawn) + var/retreat_dir = REVERSE_DIR(get_dir(pawn, target)) + for(var/i in 1 to HARASS_RETREAT_DIST) + var/turf/next = get_step(retreat_turf, retreat_dir) + if(!next || !next.can_traverse_safely(pawn) || next.density) + break + retreat_turf = next + set_movement_target(controller, retreat_turf) + +/datum/ai_behavior/human_npc_harass_retreat/perform(delta_time, datum/ai_controller/controller, target_key) + var/mob/living/carbon/human/pawn = controller.pawn + var/mob/living/target = controller.blackboard[target_key] + // Keep facing target while backing off so we don't turn our back + if(target && !QDELETED(target)) + pawn.face_atom(target) + var/turf/move_target = controller.current_movement_target + if(!move_target || get_dist(pawn, move_target) <= 1) + finish_action(controller, TRUE) + return. + +#undef HARASS_HEALTH_THRESHOLD +#undef HARASS_STAMINA_THRESHOLD +#undef HARASS_RETREAT_DIST +#undef HARASS_BASE_COOLDOWN +#undef HARASS_STAMINA_COOLDOWN_SCALE diff --git a/code/datums/ai/subtrees/human_basic_attack.dm b/code/datums/ai/subtrees/human_basic_attack.dm new file mode 100644 index 00000000000..f21bc24f604 --- /dev/null +++ b/code/datums/ai/subtrees/human_basic_attack.dm @@ -0,0 +1,332 @@ +#define HUMAN_NPC_BASE_JUKE_CHANCE 15 +#define HUMAN_NPC_JUKE_MIN_SPD 10 +#define HUMAN_NPC_JUKE_PER_OVERSPD 5 +#define HUMAN_NPC_MAX_ATTACK_STAMINA 85 +#define HUMAN_NPC_WEAKPOINT_SCAN_CHANCE 20 +#define HUMAN_NPC_WEAKPOINT_CACHE_DURATION (6 SECONDS) + +//Note alot of this is just adapted from old code so its probably not the best + +/datum/ai_planning_subtree/basic_melee_attack_subtree/human_npc + melee_attack_behavior = /datum/ai_behavior/basic_melee_attack/human_npc + end_planning = TRUE + +/datum/ai_behavior/basic_melee_attack/human_npc + action_cooldown = 0.2 SECONDS + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + +/datum/ai_behavior/basic_melee_attack/human_npc/setup(datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key) + . = ..() + if(!.) + return FALSE + var/mob/living/carbon/human/pawn = controller.pawn + var/atom/target = controller.blackboard[target_key] + + var/obj/item/held_item = pawn.get_active_held_item() + if((!isweapon(held_item))) + pawn.swap_hand() + for(var/slot in list(ITEM_SLOT_BACK, ITEM_SLOT_HIP, ITEM_SLOT_BELT_L, ITEM_SLOT_BACK_L, ITEM_SLOT_BACK_R, ITEM_SLOT_BELT_R)) + if(!pawn.get_item_by_slot(slot)) + if(pawn.equip_to_slot_if_possible(held_item, slot, disable_warning = TRUE)) + break + + var/list/possible_intents = list() + for(var/datum/intent/intent as anything in pawn.possible_a_intents) + if(istype(intent, /datum/intent/unarmed/help) || istype(intent, /datum/intent/unarmed/shove) || istype(intent, /datum/intent/unarmed/grab)) + continue + possible_intents |= intent + if(length(possible_intents)) + pawn.a_intent = pick(possible_intents) + pawn.used_intent = pawn.a_intent + + if(prob(HUMAN_NPC_WEAKPOINT_SCAN_CHANCE) && isliving(target)) + _scan_for_weakpoint(controller, pawn, target) + +/datum/ai_behavior/basic_melee_attack/human_npc/perform(delta_time, datum/ai_controller/controller, target_key, targetting_datum_key, hiding_location_key) + controller.behavior_cooldowns[src] = world.time + get_cooldown(controller) //we don't wanna call parent tbh + var/mob/living/carbon/human/pawn = controller.pawn + var/atom/target = controller.blackboard[target_key] + var/datum/targetting_datum/td = controller.blackboard[targetting_datum_key] + + if(!td.can_attack(pawn, target)) + finish_action(controller, FALSE, target_key) + return + if(ismob(target) && target:stat == DEAD) + finish_action(controller, FALSE, target_key) + return + + // Stamina gate + if(pawn.stamina > HUMAN_NPC_MAX_ATTACK_STAMINA) + finish_action(controller, FALSE, target_key) + return + + var/hiding_target = td.find_hidden_mobs(pawn, target) + controller.set_blackboard_key(hiding_location_key, hiding_target) + + pawn.face_atom(target) + _choose_attack_zone(controller, pawn, target) + + if(!pawn.CanReach(target)) + finish_action(controller, FALSE, target_key) + return + + if(hiding_target) + controller.ai_interact(hiding_target, TRUE, TRUE) + else + controller.ai_interact(target, TRUE, TRUE) + + if(pawn.next_click < world.time) + pawn.next_click = world.time + (pawn.used_intent?.clickcd * ( 1 + rand(0.2, 0.4))) + SEND_SIGNAL(pawn, COMSIG_MOB_BREAK_SNEAK) + + if(prob(HUMAN_NPC_WEAKPOINT_SCAN_CHANCE) && isliving(target)) + _scan_for_weakpoint(controller, pawn, target) + + _try_backstep(pawn, target) + +/datum/ai_behavior/basic_melee_attack/human_npc/finish_action(datum/ai_controller/controller, succeeded, target_key, targetting_datum_key, hiding_location_key) + . = ..() + var/mob/living/carbon/human/pawn = controller.pawn + pawn.cmode = FALSE + SEND_SIGNAL(pawn, COMSIG_COMBAT_TARGET_SET, FALSE) + +/datum/ai_behavior/basic_melee_attack/human_npc/proc/_choose_attack_zone(datum/ai_controller/controller, mob/living/carbon/human/pawn, mob/living/target) + var/list/wp = controller.blackboard[BB_HUMAN_NPC_WEAKPOINT] + if(wp && world.time < wp[2] && wp[3] == target) + var/aimheight = _zone_to_aimheight(wp[1]) + if(aimheight) + pawn.aimheight_change(aimheight) + return + + var/counter = controller.blackboard[BB_HUMAN_NPC_ATTACK_ZONE_COUNTER] + if(counter < 4) + controller.set_blackboard_key(BB_HUMAN_NPC_ATTACK_ZONE_COUNTER, counter + 1) + return + + controller.set_blackboard_key(BB_HUMAN_NPC_ATTACK_ZONE_COUNTER, 0) + controller.clear_blackboard_key(BB_HUMAN_NPC_WEAKPOINT) + + // Parity with npc_choose_attack_zone aimheight picks + if(pawn.mind?.has_antag_datum(/datum/antagonist/zombie)) + pawn.aimheight_change(pawn.deadite_get_aimheight(target)) + return + if(!(pawn.mobility_flags & MOBILITY_STAND)) + pawn.aimheight_change(rand(1, 4)) + return + if(HAS_TRAIT(target, TRAIT_BLOODLOSS_IMMUNE)) + pawn.aimheight_change(rand(12, 19)) + return + pawn.aimheight_change(pick(rand(5, 8), rand(9, 11), rand(12, 19))) + +/// Scan target bodyparts for wounded (brute/burn > 20) or unarmored zones. +/// Caches as list(zone, expiry_time, target_ref). +/datum/ai_behavior/basic_melee_attack/human_npc/proc/_scan_for_weakpoint(datum/ai_controller/controller, mob/living/carbon/human/pawn, mob/living/target) + if(!istype(target, /mob/living/carbon/human)) + return + var/mob/living/carbon/human/htarget = target + + // Resolve weapon skill and blade class from active intent + var/skill_type = null + var/bclass = null + var/intent_reach = 1 + if(pawn.used_intent) + bclass = pawn.used_intent.blade_class + intent_reach = pawn.used_intent.reach || 1 + // Walk held items to find associated_skill + for(var/obj/item/held in pawn.get_active_held_items()) + if(held?.associated_skill) + skill_type = held.associated_skill + break + + var/skill_level = skill_type ? pawn.get_skill_level(skill_type) : SKILL_LEVEL_NONE + var/armor_rating = bclass ? bclass_to_armor_rating(bclass) : "blunt" + + var/list/wounded = list() + var/list/exposed = list() + var/list/soft = list() // armored but below meaningful resistance for our damage type + + for(var/obj/item/bodypart/part in htarget.bodyparts) + if(!part) + continue + + //requires trained eye AND good perception + if(skill_level >= SKILL_LEVEL_JOURNEYMAN && pawn.STAPER >= 10) + if(part.brute_dam > 20 || part.burn_dam > 20) + wounded += part.body_zone + + var/obj/item/worn = htarget.get_item_by_slot(part.body_zone) + if(!worn?.armor) + exposed += part.body_zone + continue + + // Basic+ fighters read armor and seek soft coverage for their damage type + if(skill_level >= SKILL_LEVEL_NOVICE) + var/rating = worn.armor.getRating(armor_rating) + if(rating < 25) + soft += part.body_zone + // Unskilled fighters just notice bare skin + else if(!worn) + exposed += part.body_zone + + // Priority: wounded > bare exposed > soft armor coverage > armored fallback (experts only) + var/chosen = null + if(length(wounded)) + chosen = pick(wounded) + else if(length(exposed)) + chosen = pick(exposed) + else if(length(soft)) + chosen = pick(soft) + else if(skill_level >= SKILL_LEVEL_EXPERT) + // Expert fallback: just pick whatever zone has the lowest resistance for our damage type + var/lowest_rating = INFINITY + var/lowest_zone = null + for(var/obj/item/bodypart/part in htarget.bodyparts) + if(!part) + continue + var/obj/item/worn = htarget.get_item_by_slot(part.body_zone) + if(!worn?.armor) + continue + var/rating = worn.armor.getRating(armor_rating) + if(rating < lowest_rating) + lowest_rating = rating + lowest_zone = part.body_zone + chosen = lowest_zone + + if(!chosen) + return + + // Skill scales how long the targeting solution stays valid + //longer weapons can maintain solutions longer + // since the fighter isn't scrambling to stay close + var/cache_duration = HUMAN_NPC_WEAKPOINT_CACHE_DURATION + switch(skill_level) + if(SKILL_LEVEL_NONE) + cache_duration *= 0.1 + if(SKILL_LEVEL_NOVICE) + cache_duration *= 0.5 + if(SKILL_LEVEL_APPRENTICE) + cache_duration *= 0.75 + if(SKILL_LEVEL_JOURNEYMAN) + cache_duration *= 1.0 + if(SKILL_LEVEL_EXPERT) + cache_duration *= 1.5 + if(SKILL_LEVEL_MASTER) + cache_duration *= 2.0 + if(SKILL_LEVEL_LEGENDARY) + cache_duration *= 3.0 + + // Reach bonus: each point of reach beyond 1 adds 10% duration + // rationale: you're not fighting in a scramble, you have space to think + cache_duration *= (1 + ((intent_reach - 1) * 0.1)) + + controller.set_blackboard_key(BB_HUMAN_NPC_WEAKPOINT, list( + chosen, + world.time + cache_duration, + target, + )) + +/// Zone string -> aimheight int. +/datum/ai_behavior/basic_melee_attack/human_npc/proc/_zone_to_aimheight(zone) + switch(zone) + if(BODY_ZONE_HEAD) + return rand(12, 19) + if(BODY_ZONE_CHEST) + return rand(9, 11) + if(BODY_ZONE_R_ARM, BODY_ZONE_L_ARM) + return rand(5, 8) + if(BODY_ZONE_L_LEG, BODY_ZONE_R_LEG) + return rand(1, 4) + return null + +/datum/ai_behavior/basic_melee_attack/human_npc/proc/_try_backstep(mob/living/carbon/human/pawn, atom/target) + if(pawn.mind?.has_antag_datum(/datum/antagonist/zombie)) + return FALSE + if(pawn.body_position == LYING_DOWN) + return FALSE + if(pawn.ai_controller.blackboard[BB_HUMAN_NPC_HARASS_MODE]) + return FALSE + if(!target || !isturf(pawn.loc) || !isturf(target.loc)) + return FALSE + + if(world.time < pawn.ai_controller.blackboard[BB_HUMAN_NPC_JUKE_COOLDOWN]) + return FALSE + + var/juke_chance = HUMAN_NPC_BASE_JUKE_CHANCE + if(pawn.STASPD > HUMAN_NPC_JUKE_MIN_SPD) + juke_chance += (pawn.STASPD - HUMAN_NPC_JUKE_MIN_SPD) * HUMAN_NPC_JUKE_PER_OVERSPD + + if(!prob(juke_chance)) + return FALSE + + pawn.tempfixeye = TRUE + pawn.atom_flags |= NO_DIR_CHANGE_ON_MOVE + var/was_fixedeye = pawn.fixedeye + if(!was_fixedeye) + pawn.fixedeye = TRUE + + var/list/candidates = pawn.get_dodge_destinations(target, null) + if(!length(candidates)) + pawn.tempfixeye = FALSE + if(!was_fixedeye) + pawn.fixedeye = FALSE + return FALSE + + var/turf/juke_turf = pick(candidates) + pawn.Move(juke_turf, get_dir(pawn, juke_turf), pawn.cached_multiplicative_slowdown) + pawn.atom_flags &= ~NO_DIR_CHANGE_ON_MOVE + pawn.face_atom(target) + + pawn.ai_controller.set_blackboard_key(BB_HUMAN_NPC_JUKE_COOLDOWN, world.time + 1.5 SECONDS) + pawn.tempfixeye = FALSE + if(!was_fixedeye) + pawn.fixedeye = FALSE + return TRUE + +/mob/living/proc/get_dodge_destinations(mob/living/attacker, atom/origin = src) + var/dodge_dir = get_dir(attacker, origin) + if(!dodge_dir) + return null + var/list/dirry = list(turn(dodge_dir, -90), dodge_dir, turn(dodge_dir, 90)) + var/list/turf/dodge_candidates = list() + for(var/dir_to_check in dirry) + var/turf/dodge_candidate = get_step(origin, dir_to_check) + if(!dodge_candidate) + continue + if(dodge_candidate.density) + continue + var/has_impassable_atom = FALSE + for(var/atom/movable/AM in dodge_candidate) + if(!AM.CanPass(src, dodge_candidate)) + has_impassable_atom = TRUE + break + if(has_impassable_atom) + continue + dodge_candidates += dodge_candidate + return dodge_candidates + +/mob/living/carbon/human/proc/deadite_get_aimheight(victim) + if(!(mobility_flags & MOBILITY_STAND)) + return rand(1, 2) // Bite their ankles! + return pick(rand(11, 13), rand(14, 17), rand(5, 8)) // Chest, neck, and mouth; face and ears; arms and hands. + +///I couldn't find anything that does this +/proc/bclass_to_armor_rating(bclass) + switch(bclass) + if(BCLASS_BLUNT, BCLASS_SMASH, BCLASS_PUNCH, BCLASS_LASHING) + return "blunt" + if(BCLASS_CUT, BCLASS_CHOP) + return "slash" + if(BCLASS_STAB, BCLASS_DRILL, BCLASS_PICK, BCLASS_TWIST, BCLASS_BITE) + return "stab" + if(BCLASS_PIERCE, BCLASS_SHOT) + return "piercing" + if(BCLASS_BURN) + return "fire" + return "blunt" // safest fallback - everything has some blunt resistance defined + +#undef HUMAN_NPC_BASE_JUKE_CHANCE +#undef HUMAN_NPC_JUKE_MIN_SPD +#undef HUMAN_NPC_JUKE_PER_OVERSPD +#undef HUMAN_NPC_MAX_ATTACK_STAMINA +#undef HUMAN_NPC_WEAKPOINT_SCAN_CHANCE +#undef HUMAN_NPC_WEAKPOINT_CACHE_DURATION diff --git a/code/datums/ai/subtrees/loot.dm b/code/datums/ai/subtrees/loot.dm new file mode 100644 index 00000000000..4517e18893f --- /dev/null +++ b/code/datums/ai/subtrees/loot.dm @@ -0,0 +1,214 @@ + +/datum/ai_planning_subtree/loot + var/scan_range = 7 + var/scan_cooldown = 15 SECONDS + var/next_scan = 0 + +/datum/ai_planning_subtree/loot/SelectBehaviors(datum/ai_controller/controller, delta_time) + if(controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]) + return + if(controller.blackboard[BB_BASIC_MOB_FLEEING]) + return + if(next_scan > world.time) + return + next_scan = world.time + scan_cooldown + + var/mob/living/pawn = controller.pawn + var/datum/component/ai_inventory_manager/inv = controller.get_inventory() + if(!inv) + return + + var/list/blacklist = controller.blackboard[BB_LOOT_BLACKLIST] + + for(var/obj/item/candidate in view(scan_range, pawn)) + if(!isturf(candidate.loc)) + continue + if(_is_blacklisted(blacklist, candidate)) + continue + if(!_item_is_wanted(inv, pawn, candidate)) + continue + controller.set_blackboard_key(BB_LOOT_TARGET, candidate) + controller.queue_behavior(/datum/ai_behavior/loot_pick_up, BB_LOOT_TARGET) + return SUBTREE_RETURN_FINISH_PLANNING + + for(var/mob/living/corpse in orange(scan_range, pawn)) + if(corpse == pawn) + continue + if(corpse.stat != DEAD && corpse.body_position != LYING_DOWN) + continue + var/obj/item/strip_target = _find_lootable_item_on_body(inv, pawn, corpse, blacklist) + if(!strip_target) + continue + controller.set_blackboard_key(BB_LOOT_TARGET, corpse) + controller.set_blackboard_key(BB_LOOT_TARGET_ITEM, strip_target) + controller.queue_behavior(/datum/ai_behavior/loot_strip_body, BB_LOOT_TARGET, BB_LOOT_TARGET_ITEM) + return SUBTREE_RETURN_FINISH_PLANNING + +/datum/ai_planning_subtree/loot/proc/_is_blacklisted(list/blacklist, obj/item/candidate) + if(!blacklist) + return FALSE + if(candidate in blacklist) + return TRUE + return FALSE + +/datum/ai_planning_subtree/loot/proc/_item_is_wanted(datum/component/ai_inventory_manager/inv, mob/living/pawn, obj/item/candidate) + if(!candidate.flags_ai_inventory) + return FALSE + if(istype(candidate, /obj/item/gun)) + return FALSE + if(istype(candidate, /obj/item/weapon)) + return FALSE + if(candidate.anchored) + return FALSE + if(HAS_TRAIT(candidate, TRAIT_NODROP)) + return FALSE + return TRUE + +/datum/ai_planning_subtree/loot/proc/_find_lootable_item_on_body(datum/component/ai_inventory_manager/inv, mob/living/pawn, mob/living/corpse, list/blacklist) + for(var/obj/item/held in corpse.held_items) + if(!held) + continue + if(_is_blacklisted(blacklist, held)) + continue + if(!_item_is_wanted(inv, pawn, held)) + continue + if(!held.canStrip(corpse)) + continue + return held + return null + +/proc/ai_loot_blacklist_item(datum/ai_controller/controller, obj/item/it) + controller.add_blackboard_key_lazylist(BB_LOOT_BLACKLIST, it) + // Prune it after 5 minutes so the list doesn't grow forever + addtimer(CALLBACK(controller, TYPE_PROC_REF(/datum/ai_controller, remove_thing_from_blackboard_key), BB_LOOT_BLACKLIST, it), 5 MINUTES) + + +/datum/ai_behavior/loot_pick_up + action_cooldown = 0.5 SECONDS + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + var/loot_delay = 2 SECONDS + +/datum/ai_behavior/loot_pick_up/setup(datum/ai_controller/controller, target_key) + . = ..() + var/obj/item/target = controller.blackboard[target_key] + if(QDELETED(target) || !isturf(target.loc)) + return FALSE + set_movement_target(controller, target) + return TRUE + +/datum/ai_behavior/loot_pick_up/perform(delta_time, datum/ai_controller/controller, target_key) + var/obj/item/target = controller.blackboard[target_key] + if(QDELETED(target) || !isturf(target.loc)) + finish_action(controller, FALSE, target_key) + return + + var/mob/living/carbon/human/pawn = controller.pawn + if(!pawn.Adjacent(target)) + finish_action(controller, FALSE, target_key) + return + + var/datum/component/ai_inventory_manager/inv = controller.get_inventory() + if(!inv) + finish_action(controller, FALSE, target_key) + return + + if(QDELETED(target) || !isturf(target.loc)) + finish_action(controller, FALSE, target_key) + return + + var/slot_flag = inv.find_space_for(target) + if(!slot_flag) + pawn.visible_message(span_notice("[pawn] looks at [target] but has no room for it.")) + ai_loot_blacklist_item(controller, target) + finish_action(controller, FALSE, target_key) + return + + var/obj/item/container = inv.container_refs[slot_flag] + var/datum/component/storage/STR = container?.GetComponent(/datum/component/storage) + if(!STR) + finish_action(controller, FALSE, target_key) + return + + STR.handle_item_insertion(target, prevent_warning = TRUE, user = pawn) + finish_action(controller, TRUE, target_key) + +/datum/ai_behavior/loot_pick_up/finish_action(datum/ai_controller/controller, succeeded, target_key) + . = ..() + controller.clear_blackboard_key(target_key) + + +/datum/ai_behavior/loot_strip_body + action_cooldown = 0.5 SECONDS + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + var/strip_delay = 3 SECONDS + +/datum/ai_behavior/loot_strip_body/setup(datum/ai_controller/controller, body_key, item_key) + . = ..() + var/mob/living/body = controller.blackboard[body_key] + if(QDELETED(body)) + return FALSE + set_movement_target(controller, body) + return TRUE + +/datum/ai_behavior/loot_strip_body/perform(delta_time, datum/ai_controller/controller, body_key, item_key) + var/mob/living/body = controller.blackboard[body_key] + var/obj/item/target_item = controller.blackboard[item_key] + + if(QDELETED(body) || QDELETED(target_item)) + finish_action(controller, FALSE, body_key, item_key) + return + + if(body.stat != DEAD && body.body_position != LYING_DOWN) + finish_action(controller, FALSE, body_key, item_key) + return + + var/mob/living/carbon/human/pawn = controller.pawn + if(!pawn.Adjacent(body)) + finish_action(controller, FALSE, body_key, item_key) + return + + if(!target_item.canStrip(body)) + finish_action(controller, FALSE, body_key, item_key) + return + + pawn.visible_message(span_notice("[pawn] searches [body]'s body.")) + + if(!do_after(pawn, strip_delay, body)) + finish_action(controller, FALSE, body_key, item_key) + return + + if(QDELETED(body) || QDELETED(target_item)) + finish_action(controller, FALSE, body_key, item_key) + return + if(body.stat != DEAD && body.body_position != LYING_DOWN) + finish_action(controller, FALSE, body_key, item_key) + return + + var/datum/component/ai_inventory_manager/inv = controller.get_inventory() + if(!inv) + finish_action(controller, FALSE, body_key, item_key) + return + + var/slot_flag = inv.find_space_for(target_item) + if(!slot_flag) + ai_loot_blacklist_item(controller, target_item) + finish_action(controller, FALSE, body_key, item_key) + return + + var/obj/item/container = inv.container_refs[slot_flag] + var/datum/component/storage/STR = container?.GetComponent(/datum/component/storage) + if(!STR) + finish_action(controller, FALSE, body_key, item_key) + return + + if(target_item.doStrip(pawn, body)) + STR.handle_item_insertion(target_item, prevent_warning = TRUE, user = pawn) + finish_action(controller, TRUE, body_key, item_key) + else + ai_loot_blacklist_item(controller, target_item) + finish_action(controller, FALSE, body_key, item_key) + +/datum/ai_behavior/loot_strip_body/finish_action(datum/ai_controller/controller, succeeded, body_key, item_key) + . = ..() + controller.clear_blackboard_key(body_key) + controller.clear_blackboard_key(item_key) diff --git a/code/datums/ai/subtrees/retrieve_arrow.dm b/code/datums/ai/subtrees/retrieve_arrow.dm new file mode 100644 index 00000000000..0dd973057b6 --- /dev/null +++ b/code/datums/ai/subtrees/retrieve_arrow.dm @@ -0,0 +1,83 @@ +/datum/ai_planning_subtree/retrieve_arrows + parent_type = /datum/ai_planning_subtree/archer_base + +/datum/ai_planning_subtree/retrieve_arrows/SelectBehaviors(datum/ai_controller/controller, delta_time) + if(!validate_archer_equipment(controller)) + return + var/obj/item/gun/ballistic/revolver/grenadelauncher/bow/bow = controller.blackboard[BB_ARCHER_NPC_BOW] + var/obj/item/ammo_holder/quiver/Q = controller.blackboard[BB_ARCHER_NPC_QUIVER] + + if(bow.chambered) + return + if(Q.ammo_list.len >= Q.max_storage) + return + if(!controller.blackboard[BB_ARCHER_NPC_TARGET_ARROW]) + var/obj/item/arrow = _find_nearby_arrow(get_turf(controller.pawn), Q) + if(!arrow) + return + controller.set_blackboard_key(BB_ARCHER_NPC_TARGET_ARROW, arrow) + + controller.queue_behavior(/datum/ai_behavior/retrieve_arrow, BB_ARCHER_NPC_TARGET_ARROW) + return SUBTREE_RETURN_FINISH_PLANNING + +/datum/ai_planning_subtree/retrieve_arrows/proc/_find_nearby_arrow(mob/living/carbon/human/pawn, obj/item/ammo_holder/quiver/Q) + var/turf/pawn_turf = get_turf(pawn) + for(var/obj/item/ammo_casing/arrow in range(ARCHER_NPC_ARROW_SEARCH_RANGE, pawn_turf)) + for(var/accepted in Q.ammo_type) + if(istype(arrow, accepted)) + return arrow + return null + + +/datum/ai_behavior/retrieve_arrow + behavior_flags = AI_BEHAVIOR_REQUIRE_MOVEMENT | AI_BEHAVIOR_REQUIRE_REACH | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + action_cooldown = 0.5 SECONDS + +/datum/ai_behavior/retrieve_arrow/setup(datum/ai_controller/controller, arrow_key) + . = ..() + if(!.) + return FALSE + var/obj/item/arrow = controller.blackboard[arrow_key] + if(!arrow || QDELETED(arrow)) + controller.clear_blackboard_key(arrow_key) + return FALSE + controller.current_movement_target = arrow + return TRUE + +/datum/ai_behavior/retrieve_arrow/perform(delta_time, datum/ai_controller/controller, arrow_key) + var/mob/living/carbon/human/pawn = controller.pawn + var/obj/item/ammo_casing/arrow = controller.blackboard[arrow_key] + + if(!arrow || QDELETED(arrow)) + finish_action(controller, FALSE, arrow_key) + return + + if(!pawn.CanReach(arrow)) + finish_action(controller, FALSE, arrow_key) + return + + // Find the quiver again at perform time in case equipment changed + var/obj/item/ammo_holder/quiver/Q = null + for(var/obj/item/ammo_holder/quiver/worn in pawn.get_equipped_items()) + for(var/accepted in worn.ammo_type) + if(istype(arrow, accepted)) + Q = worn + break + if(Q) + break + + if(!Q || Q.ammo_list.len >= Q.max_storage) + finish_action(controller, FALSE, arrow_key) + return + + // Pick up the arrow and store directly into quiver + // Mirrors ammo_holder/attackby logic but without needing a mob intermediary since we want this to just work + arrow.forceMove(Q) + Q.ammo_list += arrow + Q.update_appearance(UPDATE_ICON_STATE) + + finish_action(controller, TRUE, arrow_key) + +/datum/ai_behavior/retrieve_arrow/finish_action(datum/ai_controller/controller, succeeded, arrow_key) + . = ..() + controller.clear_blackboard_key(arrow_key) diff --git a/code/datums/ai/subtrees/use_bandage.dm b/code/datums/ai/subtrees/use_bandage.dm new file mode 100644 index 00000000000..52b644fee10 --- /dev/null +++ b/code/datums/ai/subtrees/use_bandage.dm @@ -0,0 +1,65 @@ +/datum/ai_planning_subtree/use_bandage + +/datum/ai_planning_subtree/use_bandage/SelectBehaviors(datum/ai_controller/controller, delta_time) + if(controller.blackboard[BB_HELD_CONSUMABLE] || controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]) + return + + var/datum/component/ai_inventory_manager/inv = controller.get_inventory() + if(!inv) + return + var/obj/item/bandage = inv.get_item(AI_ITEM_BANDAGE) + if(!bandage) + return + var/real = FALSE + var/mob/living/carbon/human/human_pawn = controller.pawn + for(var/obj/item/bodypart/bodypart as anything in human_pawn.bodyparts) + if((length(bodypart.wounds) || length(bodypart.embedded_objects)) && !bodypart.bandage) + real = TRUE + controller.set_blackboard_key(BB_TARGET_ZONE_OVERRIDE, bodypart.body_zone) + break + + if(!real) + return + controller.queue_behavior(/datum/ai_behavior/apply_bandage, BB_HELD_CONSUMABLE, bandage) + return SUBTREE_RETURN_FINISH_PLANNING + +/datum/ai_behavior/apply_bandage + action_cooldown = 30 SECONDS + +/datum/ai_behavior/apply_bandage/perform(delta_time, datum/ai_controller/controller, consumable_key, obj/item/bandage) + controller.set_blackboard_key(BB_HELD_CONSUMABLE, bandage) + if(!bandage) + finish_action(controller, FALSE, consumable_key) + return + + var/datum/component/ai_inventory_manager/inv = controller.get_inventory() + var/mob/living/carbon/human/H = controller.pawn + + if(H.get_active_held_item() != bandage) + var/obj/item/usable = inv?.draw_usable_item(bandage, AI_ITEM_BANDAGE) + if(!usable) + finish_action(controller, FALSE, consumable_key) + return + + // Cache the extracted item so finish_action can clean it up + controller.set_blackboard_key(BB_HELD_CONSUMABLE, usable) + + var/old_zone = H.zone_selected + H.zone_selected = controller.blackboard[BB_TARGET_ZONE_OVERRIDE] + controller.ai_interact(H, maintain_position = TRUE) + controller.clear_blackboard_key(BB_TARGET_ZONE_OVERRIDE) + H.zone_selected = old_zone + finish_action(controller, TRUE, consumable_key) + +/datum/ai_behavior/apply_bandage/finish_action(datum/ai_controller/controller, succeeded, consumable_key) + . = ..() + controller.clear_blackboard_key(consumable_key) + var/datum/component/ai_inventory_manager/inv = controller.get_inventory() + + var/mob/living/carbon/human/H = controller.pawn + var/obj/item/held = H.get_active_held_item() + if(held && (held.flags_ai_inventory & AI_ITEM_BANDAGE)) + if(!inv?.stow_item(held)) + H.dropItemToGround(held) + + inv?.restore_hands() diff --git a/code/datums/ai/subtrees/use_explosive.dm b/code/datums/ai/subtrees/use_explosive.dm new file mode 100644 index 00000000000..1c13226b1ae --- /dev/null +++ b/code/datums/ai/subtrees/use_explosive.dm @@ -0,0 +1,63 @@ +/datum/ai_planning_subtree/throw_grenade/SelectBehaviors(datum/ai_controller/controller, delta_time) + // Only throw grenades if we have a target to throw at + var/mob/living/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + if(!target) + return + // Don't interrupt if already doing something + if(controller.blackboard[BB_HELD_CONSUMABLE]) + return + var/datum/component/ai_inventory_manager/inv = controller.get_inventory() + if(!inv) + return + var/obj/item/explosive/grenade = inv.get_item(AI_ITEM_GRENADE) + if(!grenade) + return + controller.set_blackboard_key(BB_HELD_CONSUMABLE, grenade) + controller.queue_behavior(/datum/ai_behavior/throw_grenade, BB_HELD_CONSUMABLE, BB_BASIC_MOB_CURRENT_TARGET, grenade) + +/datum/ai_behavior/throw_grenade + action_cooldown = 2 MINUTES // Long cooldown - grenades are precious and dangerous also cause fuck having smoke bomb spamming horcs + behavior_flags = AI_BEHAVIOR_MOVE_AND_PERFORM | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + +/datum/ai_behavior/throw_grenade/perform(delta_time, datum/ai_controller/controller, consumable_key, target_key, obj/item/explosive/grenade) + controller.set_blackboard_key(BB_HELD_CONSUMABLE, grenade) + if(!grenade) + finish_action(controller, FALSE, consumable_key, target_key) + return + var/mob/living/target = controller.blackboard[target_key] + if(!target || QDELETED(target)) + finish_action(controller, FALSE, consumable_key, target_key) + return + var/mob/living/carbon/human/H = controller.pawn + // Check we're in throwable range + if(get_dist(H, target) > grenade.throw_range) + finish_action(controller, FALSE, consumable_key, target_key) + return + // no throwing through walls hopefully + if(!can_see(H, target, grenade.throw_range)) + finish_action(controller, FALSE, consumable_key, target_key) + return + var/datum/component/ai_inventory_manager/inv = controller.get_inventory() + if(H.get_active_held_item() != grenade) + var/obj/item/usable = inv?.draw_usable_item(grenade, AI_ITEM_GRENADE) + if(!usable) + finish_action(controller, FALSE, consumable_key, target_key) + return + controller.set_blackboard_key(BB_HELD_CONSUMABLE, usable) + grenade = usable + grenade.arm_grenade(H) + H.throw_item(get_turf(target)) + finish_action(controller, TRUE, consumable_key, target_key) + +/datum/ai_behavior/throw_grenade/finish_action(datum/ai_controller/controller, succeeded, consumable_key, target_key) + . = ..() + controller.clear_blackboard_key(consumable_key) + controller.clear_blackboard_key(target_key) + // If somehow we still have it (arm failed, throw failed), drop it and die I guess + var/datum/component/ai_inventory_manager/inv = controller.get_inventory() + var/mob/living/carbon/human/H = controller.pawn + var/obj/item/held = H.get_active_held_item() + if(held && (held.flags_ai_inventory & AI_ITEM_GRENADE)) + if(!inv?.stow_item(held)) + H.dropItemToGround(held) + inv?.restore_hands() diff --git a/code/datums/ai/subtrees/use_healing_drink.dm b/code/datums/ai/subtrees/use_healing_drink.dm new file mode 100644 index 00000000000..4f90d2cae2e --- /dev/null +++ b/code/datums/ai/subtrees/use_healing_drink.dm @@ -0,0 +1,68 @@ +/datum/ai_planning_subtree/use_healing_drink/SelectBehaviors(datum/ai_controller/controller, delta_time) + var/datum/component/ai_inventory_manager/inv = controller.get_inventory() + if(!inv) + return + + // Find a drink that actually has reagents, purge empties along the way + var/obj/item/reagent_containers/drink = null + for(var/obj/item/reagent_containers/candidate as anything in inv.inventory_map[AI_ITEM_HEALING_DRINK]) + if(!candidate.reagents?.total_volume) + inv.drop_empty_container(candidate) + continue + drink = candidate + break + + if(!drink) + return + + var/mob/living/carbon/human/H = controller.pawn + if(H.getBruteLoss() < 20 && H.getFireLoss() < 20) + return + + controller.queue_behavior(/datum/ai_behavior/consume_healing_drink, BB_HELD_CONSUMABLE, drink) + +/datum/ai_behavior/consume_healing_drink + action_cooldown = 70 SECONDS + behavior_flags = AI_BEHAVIOR_MOVE_AND_PERFORM | AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + +/datum/ai_behavior/consume_healing_drink/perform(delta_time, datum/ai_controller/controller, consumable_key, obj/item/reagent_containers/glass/bottle/drink) + controller.set_blackboard_key(BB_HELD_CONSUMABLE, drink) + var/datum/component/ai_inventory_manager/inv = controller.get_inventory() + var/mob/living/carbon/human/H = controller.pawn + + // may have been used since queued + if(!drink || !drink.reagents?.total_volume) + if(drink) + inv?.drop_empty_container(drink) + finish_action(controller, FALSE, consumable_key) + return + + if(!inv?.draw_item(drink, AI_ITEM_HEALING_DRINK)) + finish_action(controller, FALSE, consumable_key) + return + + if(!drink.canconsume(H, H)) + finish_action(controller, FALSE, consumable_key) + return + + if(drink.closed) + drink.toggle_cork(H, FALSE) + + drink.attack(H, H, list()) + finish_action(controller, TRUE, consumable_key) + +/datum/ai_behavior/consume_healing_drink/finish_action(datum/ai_controller/controller, succeeded, consumable_key) + . = ..() + var/datum/component/ai_inventory_manager/inv = controller.get_inventory() + var/mob/living/carbon/human/H = controller.pawn + + // Check if what's now in hand is the drink we used and it's empty + var/obj/item/reagent_containers/held = H.get_active_held_item() + if(istype(held, /obj/item/reagent_containers)) + if(!held.reagents?.total_volume) + inv?.drop_empty_container(held) + else + if(!inv?.stow_item(held)) + H.dropItemToGround(held) + inv?.restore_hands() + controller.clear_blackboard_key(consumable_key) diff --git a/code/datums/ai/subtrees/use_powders.dm b/code/datums/ai/subtrees/use_powders.dm new file mode 100644 index 00000000000..f3ce661a680 --- /dev/null +++ b/code/datums/ai/subtrees/use_powders.dm @@ -0,0 +1,48 @@ +/datum/ai_planning_subtree/use_powder/SelectBehaviors(datum/ai_controller/controller, delta_time) + // ONLY use powder if we're actively fighting someone + if(!controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]) + return + // Don't interrupt if already holding something to use + if(controller.blackboard[BB_HELD_CONSUMABLE]) + return + var/datum/component/ai_inventory_manager/inv = controller.get_inventory() + if(!inv) + return + var/obj/item/powder = inv.get_item(AI_ITEM_POWDER) + if(!powder) + return + controller.queue_behavior(/datum/ai_behavior/use_powder, BB_HELD_CONSUMABLE, powder) + +/datum/ai_behavior/use_powder + action_cooldown = 3 MINUTES // Very long cooldown, this is a rare treat + +/datum/ai_behavior/use_powder/perform(delta_time, datum/ai_controller/controller, consumable_key, obj/item/powder) + controller.set_blackboard_key(BB_HELD_CONSUMABLE, powder) + if(!powder) + finish_action(controller, FALSE, consumable_key) + return + var/datum/component/ai_inventory_manager/inv = controller.get_inventory() + var/mob/living/carbon/human/H = controller.pawn + if(H.get_active_held_item() != powder) + var/obj/item/usable = inv?.draw_usable_item(powder, AI_ITEM_POWDER) + if(!usable) + finish_action(controller, FALSE, consumable_key) + return + controller.set_blackboard_key(BB_HELD_CONSUMABLE, usable) + // Powder must be snorted + var/old_zone = H.zone_selected + H.zone_selected = BODY_ZONE_PRECISE_NOSE + controller.ai_interact(H, maintain_position = TRUE) + H.zone_selected = old_zone + finish_action(controller, TRUE, consumable_key) + +/datum/ai_behavior/use_powder/finish_action(datum/ai_controller/controller, succeeded, consumable_key) + . = ..() + controller.clear_blackboard_key(consumable_key) + var/datum/component/ai_inventory_manager/inv = controller.get_inventory() + var/mob/living/carbon/human/H = controller.pawn + var/obj/item/held = H.get_active_held_item() + if(held && (held.flags_ai_inventory & AI_ITEM_POWDER)) + if(!inv?.stow_item(held)) + H.dropItemToGround(held) + inv?.restore_hands() diff --git a/code/datums/ai/subtrees/use_throwing_weapon.dm b/code/datums/ai/subtrees/use_throwing_weapon.dm new file mode 100644 index 00000000000..c7a960f811a --- /dev/null +++ b/code/datums/ai/subtrees/use_throwing_weapon.dm @@ -0,0 +1,78 @@ +/datum/ai_planning_subtree/use_throwable + var/max_throw_dist = 7 // Only throw if within this distance + var/min_throw_dist = 2 // Don't bother throwing at point blank + +/datum/ai_planning_subtree/use_throwable/SelectBehaviors(datum/ai_controller/controller, delta_time) + var/mob/living/target = controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + if(!target) + return + if(controller.blackboard[BB_HELD_CONSUMABLE]) + return + + var/datum/component/ai_inventory_manager/inv = controller.get_inventory() + if(!inv) + return + + var/obj/item/weapon/knife/throwingknife = inv.get_item(AI_ITEM_THROWING) + if(!throwingknife) + return + + var/mob/living/pawn = controller.pawn + var/dist = get_dist(pawn, target) + + // Only throw within our sweet spot range + if(dist > max_throw_dist || dist < min_throw_dist) + return + + // Need line of sight to throw + if(!can_see(pawn, target, max_throw_dist)) + return + + controller.queue_behavior(/datum/ai_behavior/use_throwable, BB_HELD_CONSUMABLE, BB_BASIC_MOB_CURRENT_TARGET, throwingknife) + +/datum/ai_behavior/use_throwable + action_cooldown = 4 SECONDS + behavior_flags = AI_BEHAVIOR_CAN_PLAN_DURING_EXECUTION + +/datum/ai_behavior/use_throwable/perform(delta_time, datum/ai_controller/controller, consumable_key, target_key, obj/item/weapon/knife/throwingknife) + . = ..() + controller.set_blackboard_key(BB_HELD_CONSUMABLE, throwingknife) + if(!throwingknife) + finish_action(controller, FALSE, consumable_key, target_key) + return + + var/mob/living/target = controller.blackboard[target_key] + if(!target || QDELETED(target)) + finish_action(controller, FALSE, consumable_key, target_key) + return + + var/mob/living/pawn = controller.pawn + var/dist = get_dist(pawn, target) + + if(dist > throwingknife.throw_range || dist < 2) + finish_action(controller, FALSE, consumable_key, target_key) + return + + if(!can_see(pawn, target, throwingknife.throw_range)) + finish_action(controller, FALSE, consumable_key, target_key) + return + + var/datum/component/ai_inventory_manager/inv = controller.get_inventory() + if(pawn.get_active_held_item() != throwingknife) + var/obj/item/usable = inv?.draw_usable_item(throwingknife, AI_ITEM_THROWING) + if(!usable) + finish_action(controller, FALSE, consumable_key, target_key) + return + controller.set_blackboard_key(BB_HELD_CONSUMABLE, usable) + throwingknife = usable + + pawn.face_atom(target) + pawn.throw_item(get_turf(target)) + finish_action(controller, TRUE, consumable_key, target_key) + +/datum/ai_behavior/use_throwable/finish_action(datum/ai_controller/controller, succeeded, consumable_key, target_key) + . = ..() + controller.clear_blackboard_key(consumable_key) + // Restore hands to whatever they were holding before + var/datum/component/ai_inventory_manager/inv = controller.get_inventory() + inv?.restore_hands() diff --git a/code/datums/antag_retainer.dm b/code/datums/antag_retainer.dm index 07a7e482b72..5feb17c7eaf 100644 --- a/code/datums/antag_retainer.dm +++ b/code/datums/antag_retainer.dm @@ -73,7 +73,8 @@ SSticker.missing_lord_time = world.time if(world.time > SSticker.missing_lord_time + 10 MINUTES) SSticker.missing_lord_time = world.time - addomen(OMEN_NOLORD) + if(!SSticker.vote_started) + addomen(OMEN_NOLORD) return FALSE else return TRUE diff --git a/code/datums/components/aggro_board.dm b/code/datums/components/aggro_board.dm index f8bc9851749..ff3f7caed8a 100644 --- a/code/datums/components/aggro_board.dm +++ b/code/datums/components/aggro_board.dm @@ -58,7 +58,7 @@ var/list/aggro_table = living_mob.ai_controller.blackboard[BB_MOB_AGGRO_TABLE] if(!length(aggro_table)) add_threat(living_mob, target, amount) - var/aggro = aggro_table[living_mob] + var/aggro = aggro_table[target] if(aggro >= cap) return amount -= aggro @@ -116,12 +116,15 @@ // Update the aggro table victim.ai_controller.blackboard[BB_MOB_AGGRO_TABLE] = aggro_table + if(!victim.ai_controller.blackboard[BB_BASIC_MOB_CURRENT_TARGET]) + victim.ai_controller.set_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET, attacker) + // Update highest threat mob update_highest_threat(victim) /// Periodically decays threat levels /datum/component/ai_aggro_system/process() - var/decay_amount = default_decay_rate * 10 + var/decay_amount = default_decay_rate var/mob/living/living_mob = parent if(!living_mob?.ai_controller) return diff --git a/code/datums/components/combat_vocalizer.dm b/code/datums/components/combat_vocalizer.dm new file mode 100644 index 00000000000..5803ec5f963 --- /dev/null +++ b/code/datums/components/combat_vocalizer.dm @@ -0,0 +1,92 @@ +/datum/component/combat_vocalizer + /// List of lines to say on aggro/during combat. Can be a list of strings or a path to a file. + var/list/aggro_lines + /// List of lines to say on death. + var/list/death_lines + /// Cooldown between combat barks (say() calls during handle_combat equivalent) + var/bark_cooldown = 8 SECONDS + /// Cooldown between emotes during combat + var/emote_cooldown = 5 SECONDS + /// Chance per planning tick to bark + var/bark_chance = 3 + /// Chance per planning tick to emote + var/emote_chance = 5 + + COOLDOWN_DECLARE(last_bark) + COOLDOWN_DECLARE(last_emote) + +/datum/component/combat_vocalizer/Initialize(list/lines, list/death_lines, bark_cooldown, emote_cooldown, bark_chance, emote_chance) + if(!isliving(parent)) + return COMPONENT_INCOMPATIBLE + src.aggro_lines = lines + src.death_lines = death_lines + if(!isnull(bark_cooldown)) + src.bark_cooldown = bark_cooldown + if(!isnull(emote_cooldown)) + src.emote_cooldown = emote_cooldown + if(!isnull(bark_chance)) + src.bark_chance = bark_chance + if(!isnull(emote_chance)) + src.emote_chance = emote_chance + + RegisterSignal(parent, COMSIG_MOB_DEATH, PROC_REF(on_death)) + RegisterSignal(parent, COMSIG_AI_BLACKBOARD_KEY_SET(BB_BASIC_MOB_CURRENT_TARGET), PROC_REF(on_target_acquired)) + RegisterSignal(parent, COMSIG_MOB_TRY_BARK, PROC_REF(try_combat_bark)) + RegisterSignal(parent, COMSIG_MOB_TRY_EMOTE, PROC_REF(try_combat_emote)) + RegisterSignal(parent, COMSIG_MOB_MODIFY_AGGRO_LINES, PROC_REF(try_modify_aggro_lines)) + RegisterSignal(parent, COMSIG_MOB_MODIFY_DEATH_LINES, PROC_REF(try_modify_death_lines)) + +/datum/component/combat_vocalizer/Destroy(force) + . = ..() + UnregisterSignal(parent, list( + COMSIG_MOB_DEATH, + COMSIG_AI_BLACKBOARD_KEY_SET(BB_BASIC_MOB_CURRENT_TARGET), + )) + +/datum/component/combat_vocalizer/proc/on_target_acquired(mob/living/source, key) + if(!COOLDOWN_FINISHED(src, last_bark)) + return + if(!length(aggro_lines)) + return + var/mob/living/target = source.ai_controller?.blackboard[BB_BASIC_MOB_CURRENT_TARGET] + if(!target) + return + source.say(pick(aggro_lines)) + source.pointed(target) + COOLDOWN_START(src, last_bark, bark_cooldown) + +/datum/component/combat_vocalizer/proc/try_combat_bark(mob/living/source) + if(!COOLDOWN_FINISHED(src, last_bark)) + return + if(!length(aggro_lines)) + return + if(!prob(bark_chance)) + return + if(aggro_lines) + source.say(pick(aggro_lines)) + COOLDOWN_START(src, last_bark, bark_cooldown) + +/datum/component/combat_vocalizer/proc/try_combat_emote(mob/living/source, emote_key) + if(!COOLDOWN_FINISHED(src, last_emote)) + return + if(!prob(emote_chance)) + return + source.emote(emote_key) + COOLDOWN_START(src, last_emote, emote_cooldown) + +/datum/component/combat_vocalizer/proc/on_death(mob/living/source) + if(!length(death_lines)) + return + if(death_lines) + source.say(pick(death_lines), forced = TRUE) + source.emote("painscream") + +/datum/component/combat_vocalizer/proc/try_modify_death_lines(mob/living/source, list/incoming_lines, override) + if(override) + death_lines = list() + death_lines += incoming_lines + +/datum/component/combat_vocalizer/proc/try_modify_aggro_lines(mob/living/source, list/incoming_lines, override) + if(override) + aggro_lines = list() + aggro_lines += incoming_lines diff --git a/code/datums/components/inventory_manager.dm b/code/datums/components/inventory_manager.dm new file mode 100644 index 00000000000..93792b9d47e --- /dev/null +++ b/code/datums/components/inventory_manager.dm @@ -0,0 +1,316 @@ +/datum/component/ai_inventory_manager + /// list(ai_item_category_flag = list(obj/item = slot_bitflag)) + var/alist/inventory_map + + /// list(slot_bitflag = obj/item), only slots containing a storage container + var/alist/container_refs + + /// Cached flat list of all slot bitflags to iterate, built once + var/static/alist/all_slot_flags + + /// What was in each hand before we drew a consumable, keyed by hand slot flag + var/obj/item/cached_inactive_hand + var/obj/item/cached_active_hand + +/datum/component/ai_inventory_manager/Initialize(mapload) + if(!iscarbon(parent)) + return COMPONENT_INCOMPATIBLE + + _build_slot_flag_list() + inventory_map = alist() + for(var/ai_item_type as anything in GLOB.ai_item_flags) + inventory_map[ai_item_type] = list() + + container_refs = alist() + + RegisterSignal(parent, COMSIG_MOB_EQUIPPED_ITEM, PROC_REF(on_equip)) + RegisterSignal(parent, COMSIG_MOB_UNEQUIPPED_ITEM, PROC_REF(on_unequip)) + RegisterSignal(parent, COMSIG_MOB_DROPITEM, PROC_REF(on_drop)) + + full_reappraise() + +/datum/component/ai_inventory_manager/Destroy() + for(var/slot in container_refs) + UnregisterSignal(container_refs[slot], COMSIG_PARENT_QDELETING) + for(var/cat in inventory_map) + for(var/obj/item/it as anything in inventory_map[cat]) + UnregisterSignal(it, COMSIG_PARENT_QDELETING) + container_refs = null + inventory_map = null + return ..() + +/datum/component/ai_inventory_manager/proc/_build_slot_flag_list() + if(all_slot_flags) + return + all_slot_flags = alist() + for(var/i in 0 to SLOTS_AMT - 1) + var/flag = (1 << i) + if(flag & AI_INVENTORY_WATCHED_SLOTS) + all_slot_flags += flag + +/datum/component/ai_inventory_manager/proc/full_reappraise() + var/mob/living/carbon/human/H = parent + + for(var/slot in container_refs) + UnregisterSignal(container_refs[slot], COMSIG_PARENT_QDELETING) + container_refs = alist() + for(var/cat in inventory_map) + for(var/obj/item/it as anything in inventory_map[cat]) + UnregisterSignal(it, COMSIG_PARENT_QDELETING) + inventory_map[cat] = list() + + for(var/slot_flag in all_slot_flags) + var/obj/item/candidate = H.get_item_by_slot(slot_flag) + if(!candidate) + continue + _try_register_container(slot_flag, candidate) + if(!candidate.GetComponent(/datum/component/storage)) + _classify_item(candidate, slot_flag) + + for(var/slot_flag in container_refs) + var/obj/item/container = container_refs[slot_flag] + var/datum/component/storage/STR = container.GetComponent(/datum/component/storage) + if(STR) + _appraise_storage(STR, slot_flag) + +/// Scan inside a single storage component and classify contents +/datum/component/ai_inventory_manager/proc/_appraise_storage(datum/component/storage/STR, slot_flag) + for(var/obj/item/it in STR.contents()) + _classify_item(it, slot_flag) + +/// Register a container slot and watch it for deletion +/datum/component/ai_inventory_manager/proc/_try_register_container(slot_flag, obj/item/candidate) + if(!candidate.GetComponent(/datum/component/storage)) + return + container_refs[slot_flag] = candidate + RegisterSignal(candidate, COMSIG_PARENT_QDELETING, PROC_REF(on_container_delete), override = TRUE) + +/// Classify a single item into all matching categories +/datum/component/ai_inventory_manager/proc/_classify_item(obj/item/it, slot_flag) + RegisterSignal(it, COMSIG_PARENT_QDELETING, PROC_REF(on_item_delete), override = TRUE) + + for(var/ai_flag in GLOB.ai_item_flags) + if(ai_flag & it.flags_ai_inventory) + inventory_map[ai_flag][it] = slot_flag + +/datum/component/ai_inventory_manager/proc/on_equip(datum/source, obj/item/equipment, slot) + SIGNAL_HANDLER + if(!(slot & AI_INVENTORY_WATCHED_SLOTS)) + return + // Partial rescan: just this slot + _purge_slot(slot) + _try_register_container(slot, equipment) + if(equipment.GetComponent(/datum/component/storage)) + var/datum/component/storage/STR = equipment.GetComponent(/datum/component/storage) + _appraise_storage(STR, slot) + else + _classify_item(equipment, slot) + +/datum/component/ai_inventory_manager/proc/on_unequip(datum/source, obj/item/equipment, slot) + SIGNAL_HANDLER + if(!(slot & AI_INVENTORY_WATCHED_SLOTS)) + return + _purge_slot(slot) + if(slot in container_refs) + UnregisterSignal(container_refs[slot], COMSIG_PARENT_QDELETING) + container_refs -= slot + +/datum/component/ai_inventory_manager/proc/on_drop(datum/source, obj/item/dropped) + SIGNAL_HANDLER + _remove_item(dropped) + +/datum/component/ai_inventory_manager/proc/on_item_delete(datum/source, force) + SIGNAL_HANDLER + UnregisterSignal(source, COMSIG_PARENT_QDELETING) + _remove_item(source) + +/datum/component/ai_inventory_manager/proc/on_container_delete(datum/source, force) + SIGNAL_HANDLER + for(var/slot_flag in container_refs) + if(container_refs[slot_flag] == source) + _purge_slot(slot_flag) + container_refs -= slot_flag + return + +/// Remove all inventory_map entries for a given slot bitflag +/datum/component/ai_inventory_manager/proc/_purge_slot(slot_flag) + for(var/cat in inventory_map) + for(var/obj/item/it as anything in inventory_map[cat]) + if(inventory_map[cat][it] == slot_flag) + UnregisterSignal(it, COMSIG_PARENT_QDELETING) + inventory_map[cat] -= it + +/datum/component/ai_inventory_manager/proc/_remove_item(obj/item/it) + UnregisterSignal(it, COMSIG_PARENT_QDELETING) + for(var/cat in inventory_map) + if(it in inventory_map[cat]) + inventory_map[cat] -= it + +/datum/component/ai_inventory_manager/proc/get_item(category) + RETURN_TYPE(/obj/item) + var/list/cat = inventory_map[category] + if(!length(cat)) + return null + return cat[1] + +/datum/component/ai_inventory_manager/proc/get_item_slot(obj/item/it, category) + return inventory_map[category]?[it] + +/datum/component/ai_inventory_manager/proc/find_space_for(obj/item/it) + for(var/slot_flag in container_refs) + var/obj/item/container = container_refs[slot_flag] + var/datum/component/storage/STR = container?.GetComponent(/datum/component/storage) + if(STR?.can_be_inserted(it, stop_messages = TRUE)) + return slot_flag + return 0 + +/datum/component/ai_inventory_manager/proc/draw_item(obj/item/it, category) + var/mob/living/carbon/human/H = parent + + cached_active_hand = H.get_active_held_item() + cached_inactive_hand = H.get_inactive_held_item() + + if(istype(cached_active_hand, /obj/item/offhand)) + var/datum/component/two_handed/twohanded = cached_inactive_hand.GetComponent(/datum/component/two_handed) + twohanded.unwield(H) + cached_active_hand = null + if(istype(cached_inactive_hand, /obj/item/offhand)) + var/datum/component/two_handed/twohanded = cached_active_hand.GetComponent(/datum/component/two_handed) + twohanded.unwield(H) + cached_inactive_hand = null + + if(!_make_hand_free()) + return FALSE + + var/slot_flag = get_item_slot(it, category) + if(!slot_flag) + return FALSE + var/obj/item/container = container_refs[slot_flag] + if(!container) + return FALSE + var/datum/component/storage/STR = container.GetComponent(/datum/component/storage) + if(!STR) + return FALSE + STR.remove_from_storage(it, H) + return H.put_in_active_hand(it) + +/datum/component/ai_inventory_manager/proc/restore_hands() + if(!cached_active_hand && !cached_inactive_hand) + return + var/mob/living/carbon/human/H = parent + + var/obj/item/active = H.get_active_held_item() + var/obj/item/inactive = H.get_inactive_held_item() + + // Snapshot and clear FIRST to prevent reentrant calls from re-running + var/obj/item/want_active = cached_active_hand + var/obj/item/want_inactive = cached_inactive_hand + cached_active_hand = null + cached_inactive_hand = null + + if(active && active != want_active && active != want_inactive) + if(!stow_item(active)) + H.dropItemToGround(active) + + if(inactive && inactive != want_active && inactive != want_inactive) + if(!stow_item(inactive)) + H.dropItemToGround(inactive) + + if(want_active && !H.get_active_held_item()) + H.put_in_active_hand(want_active) + + if(want_inactive && !H.get_inactive_held_item()) + H.swap_hand() + H.put_in_active_hand(want_inactive) + H.swap_hand() + +/datum/component/ai_inventory_manager/proc/_make_hand_free() + var/mob/living/carbon/human/H = parent + if(!H.get_active_held_item()) + return TRUE + H.swap_hand() + if(!H.get_active_held_item()) + return TRUE + var/obj/item/blocking = H.get_active_held_item() + if(stow_item(blocking)) + return TRUE + H.dropItemToGround(blocking) + return TRUE + +/datum/component/ai_inventory_manager/proc/stow_item(obj/item/it) + var/mob/living/carbon/human/H = parent + if(it.loc != H) + return FALSE + var/slot_flag = find_space_for(it) + if(!slot_flag) + return FALSE + var/obj/item/container = container_refs[slot_flag] + var/datum/component/storage/STR = container.GetComponent(/datum/component/storage) + STR.handle_item_insertion(it, prevent_warning = TRUE, user = H) + _classify_item(it, slot_flag) + return TRUE + +/// Remove an empty container from inventory tracking and drop it on the ground +/datum/component/ai_inventory_manager/proc/drop_empty_container(obj/item/reagent_containers/container) + var/mob/living/carbon/human/H = parent + _remove_item(container) + + // If it's in storage, pull it out and drop it + if(container.loc != H) + for(var/slot_flag in container_refs) + var/obj/item/storage_item = container_refs[slot_flag] + var/datum/component/storage/STR = storage_item?.GetComponent(/datum/component/storage) + if(!STR) + continue + if(container in STR.contents()) + STR.remove_from_storage(container, H) + break + + if(container.loc == H) + H.dropItemToGround(container) + +/// Returns the actual usable item (may differ from what's in inventory_map) +/datum/component/ai_inventory_manager/proc/draw_usable_item(obj/item/it, category) + var/mob/living/carbon/human/H = parent + + if(istype(it, /obj/item/natural/bundle)) + var/obj/item/natural/bundle/bundle = it + + if(!bundle.stacktype || bundle.amount <= 0) + return null + + // by spawning the stacktype item and putting it in hand (we don't use the actual handler because of random npc bs) + if(!_make_hand_free()) + return null + + var/turf/T = get_turf(H) + var/obj/item/extracted = new bundle.stacktype(T) + + if(!H.put_in_active_hand(extracted)) + qdel(extracted) + return null + + if(bundle.amount == 1) + _remove_item(bundle) + var/slot_flag = null + for(var/sf in container_refs) + var/obj/slot = container_refs[sf] + var/datum/component/storage/STR = slot?.GetComponent(/datum/component/storage) + if(STR && (bundle in STR.contents())) + slot_flag = sf + break + if(slot_flag) + var/obj/item = container_refs[slot_flag] + var/datum/component/storage/STR = item?.GetComponent(/datum/component/storage) + STR?.remove_from_storage(bundle, H) + H.dropItemToGround(bundle) + qdel(bundle) + else + bundle.amount-- + bundle.update_bundle() + + return extracted + + if(!draw_item(it, category)) + return null + return it diff --git a/code/datums/components/udder.dm b/code/datums/components/udder.dm index d1b46377d6f..d036c3a560a 100644 --- a/code/datums/components/udder.dm +++ b/code/datums/components/udder.dm @@ -105,7 +105,10 @@ return COMPONENT_NO_AFTERATTACK /obj/item/udder/proc/handle_consumption(atom/movable/food, mob/user) - COOLDOWN_START(src, require_consume_cooldown, require_consume_timer) + var/cooldown_time = require_consume_timer + if(HAS_TRAIT(udder_mob, TRAIT_ANIMAL_PRODUCTIVE)) + cooldown_time *= 0.25 + COOLDOWN_START(src, require_consume_cooldown, cooldown_time) /obj/item/udder/Destroy() . = ..() diff --git a/code/datums/elements/interrupt_on_damage.dm b/code/datums/elements/interrupt_on_damage.dm new file mode 100644 index 00000000000..e9dd8f59dfa --- /dev/null +++ b/code/datums/elements/interrupt_on_damage.dm @@ -0,0 +1,42 @@ +/datum/element/interrupt_on_damage + element_flags = ELEMENT_BESPOKE + id_arg_index = 2 + +/datum/element/interrupt_on_damage/Attach(mob/living/target) + . = ..() + if(!isliving(target)) + return ELEMENT_INCOMPATIBLE + target.AddElement(/datum/element/relay_attackers) + RegisterSignal(target, COMSIG_DO_AFTER_BEGAN, PROC_REF(on_do_after_begin)) + +/datum/element/interrupt_on_damage/Detach(mob/living/target) + . = ..() + target.RemoveElement(/datum/element/relay_attackers) + UnregisterSignal(target, list( + COMSIG_DO_AFTER_BEGAN, + COMSIG_DO_AFTER_ENDED, + COMSIG_ATOM_WAS_ATTACKED, + )) + +/datum/element/interrupt_on_damage/proc/on_do_after_begin(mob/living/source) + SIGNAL_HANDLER + RegisterSignal(source, COMSIG_ATOM_WAS_ATTACKED, PROC_REF(on_attacked), override = TRUE) + RegisterSignal(source, COMSIG_DO_AFTER_ENDED, PROC_REF(on_do_after_end), override = TRUE) + +/datum/element/interrupt_on_damage/proc/on_do_after_end(mob/living/source) + SIGNAL_HANDLER + UnregisterSignal(source, list( + COMSIG_ATOM_WAS_ATTACKED, + COMSIG_DO_AFTER_ENDED, + )) + +/datum/element/interrupt_on_damage/proc/on_attacked(mob/living/source, atom/attacker, damage) + SIGNAL_HANDLER + if(!damage) + return + UnregisterSignal(source, list( + COMSIG_ATOM_WAS_ATTACKED, + COMSIG_DO_AFTER_ENDED, + )) + source.stop_all_doing() + on_do_after_end(source) diff --git a/code/datums/elements/locked_item.dm b/code/datums/elements/locked_item.dm new file mode 100644 index 00000000000..85c0491307b --- /dev/null +++ b/code/datums/elements/locked_item.dm @@ -0,0 +1,42 @@ +/// Applied to items. Prevents mobs from equipping this item to any slot except hands +/// unless they are in the ambush or quest faction. +/datum/element/faction_restricted_equip + element_flags = ELEMENT_BESPOKE + + /// List of factions that are allowed to equip this item freely. Defaults to ambush + quest. + var/list/allowed_factions + +/datum/element/faction_restricted_equip/Attach(datum/target, list/factions) + . = ..() + if(!isitem(target)) + return ELEMENT_INCOMPATIBLE + + allowed_factions = factions || list("ambush", "quest") + RegisterSignal(target, COMSIG_ITEM_EQUIPPED, PROC_REF(on_equipped)) + RegisterSignal(target, COMSIG_PARENT_EXAMINE, PROC_REF(on_examine)) + +/datum/element/faction_restricted_equip/Detach(datum/target) + . = ..() + UnregisterSignal(target, COMSIG_ITEM_EQUIPPED) + UnregisterSignal(target, COMSIG_PARENT_EXAMINE) + +/datum/element/faction_restricted_equip/proc/on_examine(datum/source, mob/user, list/examine_text) + examine_text += span_danger("This item has engraved runes preventing it from being worn.") + +/// Checks if the mob equipping is in an allowed faction, blocks non-hand slots if not. +/datum/element/faction_restricted_equip/proc/on_equipped(obj/item/source, mob/living/user, slot) + SIGNAL_HANDLER + + // Always allow hand slots + if(slot == ITEM_SLOT_HANDS) + return + + for(var/faction in allowed_factions) + if(faction in user.faction) + return + + user.temporarilyRemoveItemFromInventory(source) + if(!user.put_in_hands(source)) + source.forceMove(get_turf(user)) + + to_chat(user, span_warning("The enscribed runes in [source] prevent it from fitting on you.")) diff --git a/code/datums/mana/leylines/_leyline.dm b/code/datums/mana/leylines/_leyline.dm index 75a38bffee7..55d0dbdf6fd 100644 --- a/code/datums/mana/leylines/_leyline.dm +++ b/code/datums/mana/leylines/_leyline.dm @@ -166,6 +166,7 @@ GLOBAL_LIST_EMPTY_TYPED(all_leylines, /datum/mana_pool/leyline) ending, icon_state = "blood", time = INFINITY, + beam_type = /obj/effect/ebeam/leyline, max_distance = world.maxx, beam_color = theme?.beam_color, beam_layer = UPPER_LEYLINE_LAYER, @@ -173,3 +174,6 @@ GLOBAL_LIST_EMPTY_TYPED(all_leylines, /datum/mana_pool/leyline) invisibility = INVISIBILITY_LEYLINES, mana_pool = src, ) + +/obj/effect/ebeam/leyline + vis_flags = VIS_HIDE diff --git a/code/datums/proximity_monitor/fields/ai_aggro_targetting.dm b/code/datums/proximity_monitor/fields/ai_aggro_targetting.dm index 83044506682..75373b454d7 100644 --- a/code/datums/proximity_monitor/fields/ai_aggro_targetting.dm +++ b/code/datums/proximity_monitor/fields/ai_aggro_targetting.dm @@ -38,10 +38,9 @@ /datum/proximity_monitor/advanced/ai_aggro_tracking/Destroy() . = ..() if(!QDELETED(controller) && owning_behavior) - controller.modify_cooldown(owning_behavior, owning_behavior.get_cooldown(controller)) + controller.modify_cooldown(owning_behavior, world.time + owning_behavior.get_cooldown(controller)) owning_behavior = null controller = null - target_key = null targeting_strategy_key = null hiding_location_key = null filter = null @@ -54,7 +53,13 @@ . = ..() if(first_build) return - owning_behavior.new_turf_found(target, controller, filter) + var/list/present = list() + for(var/atom/movable/AM in target) + present += AM + if(length(present)) + owning_behavior.new_atoms_found(present, controller, target_key, filter, hiding_location_key) + else + owning_behavior.new_turf_found(target, controller, filter) /datum/proximity_monitor/advanced/ai_aggro_tracking/field_turf_crossed(atom/movable/movable, turf/location, turf/old_location) if(!owning_behavior.atom_allowed(movable, filter, controller.pawn)) diff --git a/code/datums/proximity_monitor/fields/ai_targetting.dm b/code/datums/proximity_monitor/fields/ai_targetting.dm index cc4c7685c98..846c31ffffd 100644 --- a/code/datums/proximity_monitor/fields/ai_targetting.dm +++ b/code/datums/proximity_monitor/fields/ai_targetting.dm @@ -38,7 +38,7 @@ /datum/proximity_monitor/advanced/ai_target_tracking/Destroy() . = ..() if(!QDELETED(controller) && owning_behavior) - controller.modify_cooldown(owning_behavior, owning_behavior.get_cooldown(controller)) + controller.modify_cooldown(owning_behavior, world.time + owning_behavior.get_cooldown(controller)) owning_behavior = null controller = null target_key = null diff --git a/code/datums/threat_regions/_region.dm b/code/datums/threat_regions/_region.dm new file mode 100644 index 00000000000..da05295ca1c --- /dev/null +++ b/code/datums/threat_regions/_region.dm @@ -0,0 +1,57 @@ +/datum/threat_region + abstract_type = /datum/threat_region + var/region_name = "Generic Region Scream At Coder" + var/latent_ambush = DANGER_SAFE_FLOOR + var/min_ambush = DANGER_SAFE_FLOOR + var/max_ambush = DANGER_DIRE_LIMIT + var/fixed_ambush = FALSE // Some region like Underdark cannot be reduced in danger + var/lowpop_tick = 1 // How much ambush to tick up every iteration <= 30 pop + var/highpop_tick = 2 // How much ambush to tick up every iteration > 30 pop + var/last_natural_ambush_time = 0 + var/last_induced_ambush_time = 0 // Time between now and the previous ambush triggered by horn + +/datum/threat_region/proc/reduce_latent_ambush(amount) + if(fixed_ambush) + return + if(latent_ambush - amount < min_ambush) + latent_ambush = min_ambush + else + latent_ambush -= amount + +/datum/threat_region/proc/increase_latent_ambush(amount) + if(fixed_ambush) + return + if(latent_ambush + amount > max_ambush) + latent_ambush = max_ambush + else + latent_ambush += amount + +// Special proc because danger level is dependent on the number of latent ambush +/datum/threat_region/proc/get_danger_level() + if(latent_ambush <= DANGER_SAFE_LIMIT) + return DANGER_LEVEL_SAFE + else if(latent_ambush <= DANGER_LOW_LIMIT) + return DANGER_LEVEL_LOW + else if(latent_ambush <= DANGER_MODERATE_LIMIT) + return DANGER_LEVEL_MODERATE + else if(latent_ambush <= DANGER_DANGEROUS_LIMIT) + return DANGER_LEVEL_DANGEROUS + else if(latent_ambush <= DANGER_DIRE_LIMIT) + return DANGER_LEVEL_BLEAK + else + return DANGER_LEVEL_SAFE + +/datum/threat_region/proc/get_danger_color(level) + switch(get_danger_level()) + if(DANGER_LEVEL_SAFE) + return "#00FF00" + if(DANGER_LEVEL_LOW) + return "#FFFF00" + if(DANGER_LEVEL_MODERATE) + return "#FFA500" + if(DANGER_LEVEL_DANGEROUS) + return "#FF0000" + if(DANGER_LEVEL_BLEAK) + return "#800080" + else + return "#FFFFFF" diff --git a/code/datums/threat_regions/basin.dm b/code/datums/threat_regions/basin.dm new file mode 100644 index 00000000000..b1c37ec1153 --- /dev/null +++ b/code/datums/threat_regions/basin.dm @@ -0,0 +1,8 @@ +/datum/threat_region/basin + region_name= THREAT_REGION_BASIN + latent_ambush = DANGER_LOW_FLOOR + min_ambush = DANGER_SAFE_FLOOR + max_ambush = DANGER_DANGEROUS_LIMIT + fixed_ambush = FALSE + lowpop_tick = 1 + highpop_tick = 1 diff --git a/code/datums/threat_regions/coast.dm b/code/datums/threat_regions/coast.dm new file mode 100644 index 00000000000..5a47358823c --- /dev/null +++ b/code/datums/threat_regions/coast.dm @@ -0,0 +1,8 @@ +/datum/threat_region/coast + region_name = THREAT_REGION_COAST + latent_ambush = DANGER_MODERATE_FLOOR + min_ambush = DANGER_SAFE_FLOOR + max_ambush = DANGER_DANGEROUS_LIMIT + fixed_ambush = FALSE + lowpop_tick = 1 + highpop_tick = 1 diff --git a/code/datums/threat_regions/mount_decap.dm b/code/datums/threat_regions/mount_decap.dm new file mode 100644 index 00000000000..9f3d8c82224 --- /dev/null +++ b/code/datums/threat_regions/mount_decap.dm @@ -0,0 +1,8 @@ +/datum/threat_region/mount_decap + region_name = THREAT_REGION_MOUNT_DECAP + latent_ambush = DANGER_DANGEROUS_FLOOR + min_ambush = DANGER_MODERATE_FLOOR + max_ambush = DANGER_DIRE_LIMIT + fixed_ambush = FALSE + lowpop_tick = 1 + highpop_tick = 2 diff --git a/code/datums/threat_regions/north_grove.dm b/code/datums/threat_regions/north_grove.dm new file mode 100644 index 00000000000..6acf7e763e7 --- /dev/null +++ b/code/datums/threat_regions/north_grove.dm @@ -0,0 +1,8 @@ +/datum/threat_region/northern_grove + region_name = THREAT_REGION_NORTHERN_GROVE + latent_ambush = DANGER_MODERATE_FLOOR + min_ambush = DANGER_SAFE_FLOOR + max_ambush = DANGER_DANGEROUS_LIMIT + fixed_ambush = FALSE + lowpop_tick = 1 + highpop_tick = 1 diff --git a/code/datums/threat_regions/outer_grove.dm b/code/datums/threat_regions/outer_grove.dm new file mode 100644 index 00000000000..8847cdd81ee --- /dev/null +++ b/code/datums/threat_regions/outer_grove.dm @@ -0,0 +1,8 @@ +/datum/threat_region/outer_grove + region_name = THREAT_REGION_OUTER_GROVE + latent_ambush = DANGER_MODERATE_LIMIT + min_ambush = DANGER_MODERATE_FLOOR + max_ambush = DANGER_DIRE_LIMIT + fixed_ambush = FALSE + lowpop_tick = 1 + highpop_tick = 2 diff --git a/code/datums/threat_regions/terror_bog.dm b/code/datums/threat_regions/terror_bog.dm new file mode 100644 index 00000000000..2decb7f1f58 --- /dev/null +++ b/code/datums/threat_regions/terror_bog.dm @@ -0,0 +1,8 @@ +/datum/threat_region/terrorbog + region_name = THREAT_REGION_TERRORBOG + latent_ambush = DANGER_DIRE_LIMIT + min_ambush = DANGER_SAFE_FLOOR // This is intended. A warden can engage in a long war to tame the terrorbog. + max_ambush = DANGER_DIRE_LIMIT + fixed_ambush = FALSE + lowpop_tick = 1 + highpop_tick = 2 diff --git a/code/datums/world_factions/traders/trade_data_types/zhongese.dm b/code/datums/world_factions/traders/trade_data_types/zhongese.dm index 43dd0b46ade..570e31a8ef3 100644 --- a/code/datums/world_factions/traders/trade_data_types/zhongese.dm +++ b/code/datums/world_factions/traders/trade_data_types/zhongese.dm @@ -95,7 +95,7 @@ /obj/item/weapon/sword/katana/mulyeog/rumahench = list(4, 65, 2), /obj/item/weapon/sword/katana/mulyeog/rumacaptain = list(6, 120, 1), /obj/item/weapon/sword/sabre/hook = list(5, 85, 2), - /obj/item/weapon/spear/naginata = list(6, 90, 2), + /obj/item/weapon/polearm/spear/naginata = list(6, 90, 2), /obj/item/weapon/knife/dagger/navaja = list(6, 75, 1), /obj/item/weapon/whip/nagaika = list(5, 60, 2), ) diff --git a/code/game/area/areas.dm b/code/game/area/areas.dm index 5196fd6f6d5..e3cdb20f4b9 100644 --- a/code/game/area/areas.dm +++ b/code/game/area/areas.dm @@ -87,6 +87,9 @@ var/list/ambush_times var/converted_type + + var/threat_region = "" // Key used to look up threat region this area belongs to + var/delver_restrictions = FALSE var/coven_protected = FALSE diff --git a/code/game/area/roguetownareas.dm b/code/game/area/roguetownareas.dm index 51e67cf667c..ee8d66eef25 100644 --- a/code/game/area/roguetownareas.dm +++ b/code/game/area/roguetownareas.dm @@ -67,9 +67,16 @@ ambush_types = list( /turf/open/floor/dirt) ambush_mobs = list( - /mob/living/simple_animal/hostile/retaliate/troll = 30, - /mob/living/carbon/human/species/skeleton/npc/ambush = 30, - /mob/living/carbon/human/species/goblin/npc/ambush/cave = 60) + new /datum/ambush_config/pair_of_direbear = 10, + new /datum/ambush_config/trio_of_highwaymen = 10, + new /datum/ambush_config/singular_minotaur = 10, + new /datum/ambush_config/duo_minotaur = 5, + new /datum/ambush_config/solo_treasure_hunter = 15, + new /datum/ambush_config/duo_treasure_hunter = 2, + new /datum/ambush_config/medium_skeleton_party = 10, + new /datum/ambush_config/heavy_skeleton_party = 5, + ) + threat_region = THREAT_REGION_MOUNT_DECAP /area/outdoors/mountains/decap name = "mt decapitation" @@ -77,9 +84,15 @@ ambush_types = list( /turf/open/floor/dirt) ambush_mobs = list( - /mob/living/simple_animal/hostile/retaliate/troll = 30, - /mob/living/carbon/human/species/skeleton/npc/ambush = 90, - /mob/living/carbon/human/species/goblin/npc/ambush/cave = 20) + new /datum/ambush_config/pair_of_direbear = 10, + new /datum/ambush_config/trio_of_highwaymen = 10, + new /datum/ambush_config/singular_minotaur = 10, + new /datum/ambush_config/duo_minotaur = 5, + new /datum/ambush_config/solo_treasure_hunter = 15, + new /datum/ambush_config/duo_treasure_hunter = 2, + new /datum/ambush_config/medium_skeleton_party = 10, + new /datum/ambush_config/heavy_skeleton_party = 5, + ) background_track = 'sound/music/area/decap.ogg' background_track_dusk = null background_track_night = null @@ -88,11 +101,14 @@ ambush_times = list("night","dawn","dusk","day") converted_type = /area/indoors/shelter/mountains/decap + threat_region = THREAT_REGION_MOUNT_DECAP + /area/indoors/shelter/mountains/decap icon_state = "decap" background_track = 'sound/music/area/decap.ogg' background_track_dusk = null background_track_night = null + threat_region = THREAT_REGION_MOUNT_DECAP /area/outdoors/basin name = "town basin" @@ -110,6 +126,7 @@ background_track_dusk = 'sound/music/area/septimus.ogg' background_track_night = 'sound/music/area/sleeping.ogg' converted_type = /area/indoors/shelter/basin + threat_region = THREAT_REGION_MOUNT_DECAP /area/outdoors/basin/Initialize() . = ..() @@ -124,6 +141,7 @@ background_track = 'sound/music/area/field.ogg' background_track_dusk = 'sound/music/area/septimus.ogg' background_track_night = 'sound/music/area/sleeping.ogg' + threat_region = THREAT_REGION_MOUNT_DECAP /area/indoors/shelter/woods icon_state = "woods" @@ -174,11 +192,18 @@ /mob/living/simple_animal/hostile/retaliate/bigrat = 20, /mob/living/simple_animal/hostile/retaliate/spider = 80, /mob/living/carbon/human/species/goblin/npc/ambush/sea = 50, - /mob/living/simple_animal/hostile/retaliate/troll/bog = 35) + /mob/living/simple_animal/hostile/retaliate/troll/bog = 35, + new /datum/ambush_config/bog_guard_deserters = 50, + new /datum/ambush_config/bog_guard_deserters/hard = 25, + new /datum/ambush_config/mirespiders_ambush = 110, + new /datum/ambush_config/mirespiders_crawlers = 25, + new /datum/ambush_config/mirespiders_aragn = 10, + new /datum/ambush_config/mirespiders_unfair = 5) first_time_text = "THE TERRORBOG" custom_area_sound = 'sound/misc/stings/BogSting.ogg' converted_type = /area/indoors/shelter/bog + threat_region = THREAT_REGION_TERRORBOG /area/indoors/shelter/bog icon_state = "bog" @@ -194,6 +219,14 @@ background_track_dusk = 'sound/music/area/septimus.ogg' background_track_night = 'sound/music/area/sleeping.ogg' + ambush_mobs = list( + /mob/living/carbon/human/species/goblin/npc/ambush/sea = 20, + new /datum/ambush_config/triple_deepone = 30, + new /datum/ambush_config/deepone_party = 20, + ) + + threat_region = THREAT_REGION_COAST + /area/outdoors/eora name = "eoran grove" icon_state = "eora" diff --git a/code/game/area/wilderness.dm b/code/game/area/wilderness.dm index cc4e042238f..fdaefbcc2c3 100644 --- a/code/game/area/wilderness.dm +++ b/code/game/area/wilderness.dm @@ -12,21 +12,33 @@ ambush_times = list("night","dawn","dusk","day") ambush_types = list( /turf/open/floor/grass) + ambush_mobs = list( - /mob/living/simple_animal/hostile/retaliate/wolf = 60, - /mob/living/simple_animal/hostile/retaliate/troll/axe = 10, - /mob/living/carbon/human/species/goblin/npc/ambush = 45, - /mob/living/simple_animal/hostile/retaliate/mole = 25) + new /datum/ambush_config/wolf_pack = 15, + new /datum/ambush_config/lone_troll = 10, + new /datum/ambush_config/troll_and_wolves = 8, + new /datum/ambush_config/goblin_ambush_party = 15, + new /datum/ambush_config/goblin_raid_party = 8, + new /datum/ambush_config/raccoon_swarm = 20, + new /datum/ambush_config/mole_pack = 15, + new /datum/ambush_config/deserter_patrol = 12, + new /datum/ambush_config/highwayman_duo = 10, + new /datum/ambush_config/highwayman_gang = 6, + new /datum/ambush_config/mixed_wildlife = 15, + ) first_time_text = "THE MURDERWOOD" custom_area_sound = 'sound/misc/stings/ForestSting.ogg' converted_type = /area/indoors/shelter/woods + threat_region = THREAT_REGION_OUTER_GROVE /area/outdoors/wilderness/outpost icon_state = "outpost" + threat_region = THREAT_REGION_NORTHERN_GROVE /area/outdoors/wilderness/outpost/vanderlin name = "abandoned outpost" first_time_text = "Thatchwood Outpost" + threat_region = THREAT_REGION_NORTHERN_GROVE /area/outdoors/wilderness/outpost/salem name = "salem outpost" diff --git a/code/game/objects/effects/glowshroom.dm b/code/game/objects/effects/glowshroom.dm index d2a64288e04..13d916b7bae 100644 --- a/code/game/objects/effects/glowshroom.dm +++ b/code/game/objects/effects/glowshroom.dm @@ -37,7 +37,7 @@ if(L.electrocute_act(30, src)) L.emote("painscream") L.update_sneak_invis(TRUE) - L.consider_ambush() + L.consider_ambush(always = TRUE) if(L.throwing) L.throwing.finalize(FALSE) . = ..() @@ -48,7 +48,7 @@ var/mob/living/L = user if(L.electrocute_act(30, src)) // The kneestingers will let you pass if you worship dendor, but they won't take your stupid ass hitting them. L.emote("painscream") - L.consider_ambush() + L.consider_ambush(always = TRUE) if(L.throwing) L.throwing.finalize(FALSE) return FALSE diff --git a/code/game/objects/items.dm b/code/game/objects/items.dm index 247f3a513fd..da2ea7abdab 100644 --- a/code/game/objects/items.dm +++ b/code/game/objects/items.dm @@ -31,6 +31,8 @@ GLOBAL_DATUM_INIT(fire_overlay, /mutable_appearance, mutable_appearance('icons/e var/inhand_x_dimension = 64 var/inhand_y_dimension = 64 + var/flags_ai_inventory = NONE + var/no_effect = FALSE max_integrity = 200 @@ -826,6 +828,7 @@ GLOBAL_DATUM_INIT(fire_overlay, /mutable_appearance, mutable_appearance('icons/e animate(src, pixel_y = oldy, time = 0.5) item_flags &= ~IN_INVENTORY SEND_SIGNAL(src, COMSIG_ITEM_DROPPED,user) + SEND_SIGNAL(user, COMSIG_MOB_DROPITEM,src) if(!silent) playsound(src, drop_sound, DROP_SOUND_VOLUME, TRUE, ignore_walls = FALSE) toggle_altgrip(user, FALSE) @@ -1341,6 +1344,14 @@ GLOBAL_DATUM_INIT(fire_overlay, /mutable_appearance, mutable_appearance('icons/e return TRUE /obj/item/proc/canStrip(mob/stripper, mob/owner) + if(HAS_TRAIT(loc, TRAIT_STUCKITEMS)) + return FALSE + if(HAS_TRAIT(loc, TRAIT_HIGHVALUE_STUCK)) + if(melting_material == /datum/material/steel) + return FALSE + if(item_flags & HIGH_VALUE) + return FALSE + return !HAS_TRAIT(src, TRAIT_NODROP) /obj/item/proc/doStrip(mob/stripper, mob/owner) diff --git a/code/game/objects/items/barding.dm b/code/game/objects/items/barding.dm new file mode 100644 index 00000000000..0312415ca19 --- /dev/null +++ b/code/game/objects/items/barding.dm @@ -0,0 +1,104 @@ +/obj/item/clothing/barding + name = "padded barding" + desc = "A set of padded body armor for a Saiga, designed to protect your mount's vital organs." + slot_flags = null + icon = 'icons/roguetown/items/misc.dmi' + icon_state = "sewingkit" + var/barding_icon = 'icons/roguetown/mob/monster/saiga.dmi' + var/barding_state = "barding" + var/female_barding_state = "barding-f" + gender = NEUTER + var/list/valid_animal_types = list( + /mob/living/simple_animal/hostile/retaliate/saiga + ) + armor = ARMOR_PADDED_GOOD + max_integrity = ARMOR_INT_CHEST_LIGHT_MASTER + break_sound = 'sound/foley/cloth_rip.ogg' + drop_sound = 'sound/foley/dropsound/cloth_drop.ogg' + sewrepair = TRUE + salvage_result = /obj/item/natural/cloth + salvage_amount = 1 + fiber_salvage = TRUE + integrity_failure = 0.1 + +/obj/item/clothing/barding/attack(mob/living/M, mob/living/user) + if(!istype(M, /mob/living/simple_animal)) + to_chat(user, span_warning("\The [src] can only be used on animals!")) + return + if(!is_type_in_list(M, valid_animal_types)) + to_chat(user, span_warning("\The [src] cannot be used on [M]! It is only meant for specific animals.")) + return + + var/mob/living/simple_animal/animal = M + if(animal.adult_growth) + to_chat(user, span_warning("[animal] is a juvenile and cannot wear a bard!")) + return + if(animal.bbarding) + to_chat(user, span_warning("[animal] is already wearing a bard!")) + return + if(!animal.ssaddle) + to_chat(user, span_warning("[animal] needs to be saddled before you can fit a bard onto it!")) + return + + user.visible_message(span_notice("[user] is fitting a bard onto [animal]..."), span_notice("I start fitting a bard onto [animal]...")) + if(!do_after(user, 5 SECONDS, animal)) + return + + animal.bbarding = src + forceMove(animal) + animal.update_icon() + user.visible_message(span_notice("[user] fits a bard onto [animal]."), span_notice("I fit a bard onto [animal].")) + +/obj/item/clothing/barding/atom_break(damage_flag) + . = ..() + if(istype(loc, /mob/living/simple_animal)) + var/mob/living/simple_animal/A = loc + if(A.bbarding == src) + A.bbarding = null + . = ..() + +/obj/item/clothing/barding/chain + name = "chainmail barding" + desc = "A set of chainmail body armor for a Saiga, designed to protect your mount's vital organs." + icon_state = "armorkit" + barding_state = "barding_chain" + female_barding_state = "barding_chain-f" + armor = ARMOR_MAILLE + max_integrity = ARMOR_INT_CHEST_MEDIUM_STEEL + drop_sound = 'sound/foley/dropsound/chain_drop.ogg' + pickup_sound = 'sound/foley/equip/equip_armor_chain.ogg' + anvilrepair = /datum/skill/craft/armorsmithing + smeltresult = /obj/item/ingot/steel + sewrepair = FALSE + salvage_result = null + salvage_amount = 0 + fiber_salvage = FALSE + +/obj/item/clothing/barding/honse + name = "padded barding" + desc = "A set of padded body armor for a Honse, designed to protect your mount's vital organs." + icon_state = "sewingkit" + barding_icon = 'icons/mob/monster/fogbeast.dmi' + barding_state = "barding" + female_barding_state = "barding" + valid_animal_types = list( + /mob/living/simple_animal/hostile/retaliate/honse + ) + +/obj/item/clothing/barding/honse/chain + name = "chainmail barding" + desc = "A set of chainmail body armor for a Honse, designed to protect your mount's vital organs." + icon_state = "armorkit" + barding_state = "barding_chain" + female_barding_state = "barding_chain" + armor = ARMOR_MAILLE + max_integrity = ARMOR_INT_CHEST_MEDIUM_STEEL + drop_sound = 'sound/foley/dropsound/chain_drop.ogg' + pickup_sound = 'sound/foley/equip/equip_armor_chain.ogg' + anvilrepair = /datum/skill/craft/armorsmithing + melting_material = /datum/material/steel + melt_amount = 80 + sewrepair = FALSE + salvage_result = null + salvage_amount = 0 + fiber_salvage = FALSE diff --git a/code/game/objects/items/caparison.dm b/code/game/objects/items/caparison.dm new file mode 100644 index 00000000000..186f1abbcdb --- /dev/null +++ b/code/game/objects/items/caparison.dm @@ -0,0 +1,132 @@ +/obj/item/caparison + name = "caparison" + desc = "A decorative piece of cloth meant to be used as a saddle decoration. This one fits on a Saiga." + icon = 'icons/roguetown/items/misc.dmi' + icon_state = "caparison" + var/caparison_icon = 'icons/roguetown/mob/monster/saiga.dmi' + var/caparison_state = "caparison" + var/detail_state + var/list/detail_types + var/list/symbol_types + var/female_caparison_state = "caparison-f" + gender = NEUTER + var/list/valid_animal_types = list(/mob/living/simple_animal/hostile/retaliate/saiga) + +/obj/item/caparison/attack(mob/living/M, mob/living/user) + if(!istype(M, /mob/living/simple_animal)) + to_chat(user, span_warning("\The [src] can only be used on animals!")) + return + if(!is_type_in_list(M, valid_animal_types)) + to_chat(user, span_warning("\The [src] cannot be used on [M]! It is only meant for specific animals.")) + return + + var/mob/living/simple_animal/animal = M + if(animal.adult_growth) + to_chat(user, span_warning("[animal] is a juvenile and cannot wear a caparison!")) + return + if(animal.ccaparison) + to_chat(user, span_warning("[animal] is already wearing a caparison!")) + return + if(!animal.ssaddle) + to_chat(user, span_warning("[animal] needs to be saddled before you can fit a caparison onto it!")) + return + + user.visible_message(span_notice("[user] is fitting a caparison onto [animal]..."), span_notice("I start fitting a caparison onto [animal]...")) + if(!do_after(user, 5 SECONDS, animal)) + return + + animal.ccaparison = src + forceMove(animal) + animal.update_icon() + user.visible_message(span_notice("[user] fits a caparison onto [animal]."), span_notice("I fit a caparison onto [animal].")) + + +/obj/item/caparison/attack_hand_secondary(mob/user, list/modifiers) + . = ..() + if(!length(detail_types)) + return + + var/list/possible_detail_types = list("None" = null) + detail_types.Copy() + if(length(symbol_types)) + possible_detail_types += list("Symbol" = null) + + var/chosen_design = input(user, "Select a design.", "Caparison Design") as null|anything in possible_detail_types + if(!chosen_design) + return + + if(chosen_design == "Symbol") + var/chosen_symbol = input(user, "Select a symbol.", "Caparison Design") as null|anything in symbol_types + if(!chosen_symbol) + return + detail_state = symbol_types[chosen_symbol] + else + detail_state = detail_types[chosen_design] + + var/list/colors_to_pick = list() + if(GLOB.lordprimary) + colors_to_pick["Primary Keep Color"] = GLOB.lordprimary + if(GLOB.lordsecondary) + colors_to_pick["Secondary Keep Color"] = GLOB.lordsecondary + var/list/color_map_list = COLOR_MAP + colors_to_pick += color_map_list.Copy() + + var/primary_color = input(user, "Select a primary color.", "Caparison Design") as null|anything in colors_to_pick + if(!primary_color) + return + color = colors_to_pick[primary_color] + + if(chosen_design != "None") + if(chosen_design != "Symbol") + var/secondary_color = input(user, "Select a secondary color.", "Caparison Design") as null|anything in colors_to_pick + if(!secondary_color) + return + detail_color = colors_to_pick[secondary_color] + else + detail_color = COLOR_WHITE + +////////////////////// +// SUBTYPES - SAIGA // +////////////////////// + +/obj/item/caparison/psy + name = "psydonite caparison" + desc = "A decorative piece of cloth meant to be used as a saddle decoration. It's adorned with Psycrosses. This one fits on a Saiga." + caparison_state = "psy_caparison" + female_caparison_state = "psy_caparison-f" + +/obj/item/caparison/astrata + name = "astratan caparison" + desc = "A decorative piece of cloth meant to be used as a saddle decoration. It's adorned with Astratan crosses. This one fits on a Saiga." + caparison_state = "astra_caparison" + female_caparison_state = "astra_caparison-f" + +/obj/item/caparison/eora + name = "eoran caparison" + desc = "A decorative piece of cloth meant to be used as a saddle decoration. It's adorned with Eoran hearts. This one fits on a Saiga." + caparison_state = "eora_caparison" + female_caparison_state = "eora_caparison-f" + +/obj/item/caparison/azure + name = "azurean caparison" + desc = "A decorative piece of cloth meant to be used as a saddle decoration. It's adorned with ducal colours. This one fits on a Saiga." + caparison_state = "azure_caparison" + female_caparison_state = "azure_caparison-f" + +/obj/item/caparison/heartfelt + name = "Heartfelt caparison" + desc = "A decorative piece of cloth meant to be used as a saddle decoration. It's adorned with the colours of Heartfelt. This one fits on a Saiga." + caparison_state = "heartfelt_caparison" + female_caparison_state = "heartfelt_caparison-f" + +///////////////////////// +// SUBTYPES - HONSE // +///////////////////////// + +/obj/item/caparison/honse + name = "caparison" + desc = "A decorative piece of cloth meant to be used as a saddle decoration. This one fits on a Honse." + caparison_icon = 'icons/mob/monster/fogbeast.dmi' + valid_animal_types = list(/mob/living/simple_animal/hostile/retaliate/honse) + color = COLOR_WHITE + detail_types = list("Quad" = "quad") + symbol_types = list("Psycross" = "psycross", "Astrata" = "astrata") diff --git a/code/game/objects/items/cigs_lighters.dm b/code/game/objects/items/cigs_lighters.dm index 02de9947151..c547780e7e5 100644 --- a/code/game/objects/items/cigs_lighters.dm +++ b/code/game/objects/items/cigs_lighters.dm @@ -160,6 +160,10 @@ CIGARETTE PACKETS ARE IN FANCY.DM create_reagents(chem_volume, INJECTABLE | NO_REACT) if(list_reagents) reagents.add_reagent_list(list_reagents) + for(var/datum/reagent/reagent_type as anything in list_reagents) + if(ispath(reagent_type, /datum/reagent/medicine)) + flags_ai_inventory |= AI_ITEM_HEALING_DRINK + if(starts_lit) light() AddComponent(/datum/component/knockoff, 90, list(BODY_ZONE_PRECISE_MOUTH) ,list(ITEM_SLOT_MOUTH))//90% to knock off when wearing a mask diff --git a/code/game/objects/items/explosives/_base.dm b/code/game/objects/items/explosives/_base.dm index c9147929b30..f029434d5d6 100644 --- a/code/game/objects/items/explosives/_base.dm +++ b/code/game/objects/items/explosives/_base.dm @@ -21,6 +21,8 @@ grid_height = 64 grid_width = 32 + flags_ai_inventory = AI_ITEM_GRENADE + ///do we explode on impact? var/impact_explode = FALSE ///odds we fail on ignite diff --git a/code/game/objects/items/natural/cloth.dm b/code/game/objects/items/natural/cloth.dm index 4c2cac7f90c..90237523124 100644 --- a/code/game/objects/items/natural/cloth.dm +++ b/code/game/objects/items/natural/cloth.dm @@ -15,6 +15,7 @@ w_class = WEIGHT_CLASS_TINY spitoutmouth = FALSE bundletype = /obj/item/natural/bundle/cloth + flags_ai_inventory = AI_ITEM_BANDAGE var/datum/component/cleaner/cleaner_component = null var/clean_speed = 0.4 SECONDS diff --git a/code/game/objects/items/natural/clothfibersthorn.dm b/code/game/objects/items/natural/clothfibersthorn.dm index 222c11a85fc..17a11eac8ae 100644 --- a/code/game/objects/items/natural/clothfibersthorn.dm +++ b/code/game/objects/items/natural/clothfibersthorn.dm @@ -143,6 +143,7 @@ icon1step = 5 icon2 = "clothroll2" icon2step = 10 + flags_ai_inventory = AI_ITEM_BANDAGE /obj/item/natural/bundle/cloth/full/Initialize() . = ..() diff --git a/code/game/objects/items/signal_horn.dm b/code/game/objects/items/signal_horn.dm index 071b99bb17b..de46e262736 100644 --- a/code/game/objects/items/signal_horn.dm +++ b/code/game/objects/items/signal_horn.dm @@ -1,3 +1,21 @@ +#define WARDEN_AMBUSH_MIN 2 +#define WARDEN_AMBUSH_MAX 9 + +/datum/status_effect/debuff/clickcd + id = "clickcd" + alert_type = /atom/movable/screen/alert/status_effect/debuff/clickcd + duration = 3 SECONDS + +/datum/status_effect/debuff/clickcd/on_creation(mob/living/new_owner, new_dur) + if(new_dur) + duration = new_dur + new_owner.changeNext_move(duration) + return ..() + +/atom/movable/screen/alert/status_effect/debuff/clickcd + name = "Action Delayed" + desc = "I cannot take another action." + /obj/item/signal_horn name = "signal horn" desc = "Used to sound the alarm." @@ -9,18 +27,36 @@ grid_width = 64 COOLDOWN_DECLARE(sound_horn) +/obj/item/signal_horn/examine() + . = ..() + . += span_notice("Using the horn will make you stand still and induce several ambushes to happen at once, enabling you to clear out an area. It cannot be used in rapid succession.") + . += span_notice("Using it will leave you exhausted for a moment. Bring friends!") + /obj/item/signal_horn/attack_self(mob/living/user, list/modifiers) . = ..() - if(!COOLDOWN_FINISHED(src, sound_horn)) - to_chat(user, span_warning("[src] is not ready to be used yet!")) + var/area/AR = get_area(user) + var/datum/threat_region/TR = SSregionthreat.get_region(AR.threat_region) + if(!TR || !TR.latent_ambush || TR.fixed_ambush) + to_chat(user, span_warning("There's no point in sounding the horn here.")) return - user.visible_message(span_warning("[user] is about to sound [src]!")) - if(do_after(user, 1.5 SECONDS)) + if(user.get_will_block_ambush()) + to_chat(user, span_warning("This place is too well-lit for enemies to come.")) + return + if(!user.get_possible_ambush_spawn(min_dist = WARDEN_AMBUSH_MIN, max_dist = WARDEN_AMBUSH_MAX)) + to_chat(user, span_warning("This place is too lightly vegetated for enemies to hide.")) + return + if(TR && TR.last_induced_ambush_time && (world.time < TR.last_induced_ambush_time + 5 MINUTES)) + to_chat(user, span_warning("Foes have been cleared out here recently, perhaps you should wait a moment before sounding the horn again.")) + return + user.visible_message(span_userdanger("[user] is about to sound [src]!")) + user.apply_status_effect(/datum/status_effect/debuff/clickcd, 5 SECONDS) // We don't want them to spam the message. + if(do_after(user, 30 SECONDS)) // Enough time for any antag to kick or interrupt third party, me think + TR.last_induced_ambush_time = world.time + user.Immobilize(30) // A very crude solution to kill any solo gamer sound_horn(user) - COOLDOWN_START(src, sound_horn, 1 MINUTES) /obj/item/signal_horn/proc/sound_horn(mob/living/user) - user.visible_message(span_warning("[user] sounds the alarm!")) + user.visible_message(span_danger("[user] sounds the horn!")) // New sound made by fem_tanyl playsound(src, 'sound/items/signalhorn.ogg', 100, TRUE) var/turf/origin_turf = get_turf(src) @@ -34,7 +70,7 @@ continue var/distance = get_dist(player, origin_turf) - if(distance <= 7) + if(distance <= 7 || distance > 21) // two screens away player.apply_status_effect(/datum/status_effect/signal_horn, null, user) continue var/dirtext = " to the " @@ -75,6 +111,11 @@ player.playsound_local(get_turf(player), 'sound/items/signalhorn.ogg', 35, FALSE, pressure_affected = FALSE) to_chat(player, span_warning("I hear the horn alarm somewhere[disttext][dirtext]!")) + var/random_ambushes = 4 + rand(0,2) // 4 - 6 ambushes + for(var/i = 0, i < random_ambushes, i++) + user.consider_ambush(TRUE, TRUE, min_dist = WARDEN_AMBUSH_MIN, max_dist = WARDEN_AMBUSH_MAX) + + /datum/status_effect/signal_horn id = "signal horn indicator" duration = 2 SECONDS @@ -116,3 +157,6 @@ icon_state = "signal_horn_indicator" screen_loc = "CENTER:-16,CENTER:-16" alpha = 100 + +#undef WARDEN_AMBUSH_MIN +#undef WARDEN_AMBUSH_MAX diff --git a/code/game/objects/items/weapons/melee/knives.dm b/code/game/objects/items/weapons/melee/knives.dm index b1c45d262ed..593c18f4b01 100644 --- a/code/game/objects/items/weapons/melee/knives.dm +++ b/code/game/objects/items/weapons/melee/knives.dm @@ -601,6 +601,7 @@ embedding = list("embedded_pain_multiplier" = 4, "embed_chance" = 30, "embedded_fall_chance" = 20) melt_amount = 50 sellprice = 3 + flags_ai_inventory = AI_ITEM_THROWING /obj/item/weapon/knife/throwingknife/bronze name = "bronze tossblade" diff --git a/code/game/objects/items/weapons/melee/polearms.dm b/code/game/objects/items/weapons/melee/polearms.dm index b13eb28f701..19a379eaf9b 100644 --- a/code/game/objects/items/weapons/melee/polearms.dm +++ b/code/game/objects/items/weapons/melee/polearms.dm @@ -600,7 +600,7 @@ w_class = WEIGHT_CLASS_BULKY melting_material = null -/obj/item/weapon/spear/naginata +/obj/item/weapon/polearm/spear/naginata name = "Naginata" desc = "A traditional eastern polearm, combining the reach of a spear with the cutting power of a curved blade. Due to the brittle quality of certain eastern bladesmithing, weaponsmiths have adapted its blade to be easily replaceable when broken by a peg upon the end of the shaft." icon = 'icons/roguetown/weapons/64/polearms.dmi' @@ -612,7 +612,7 @@ max_blade_int = 100 //Nippon suteeru (dogshit) minstr = 7 -/obj/item/weapon/spear/naginata/getonmobprop(tag) +/obj/item/weapon/polearm/spear/naginata/getonmobprop(tag) . = ..() if(tag) switch(tag) diff --git a/code/game/objects/items/weapons/ranged/bows.dm b/code/game/objects/items/weapons/ranged/bows.dm index b69dd9bf6c5..61518a64f25 100644 --- a/code/game/objects/items/weapons/ranged/bows.dm +++ b/code/game/objects/items/weapons/ranged/bows.dm @@ -79,8 +79,12 @@ spread = 0 for(var/obj/item/ammo_casing/CB in get_ammo_list(FALSE, TRUE)) var/obj/projectile/BB = CB.BB - if(user.client.chargedprog < 100) - BB.damage = BB.damage - (BB.damage * (user.client.chargedprog / 100)) + if(user.client?.chargedprog < 100) + var/charge_prob = 1 + if(user.client) + charge_prob = (user.client.chargedprog / 100) + + BB.damage = BB.damage - (BB.damage * charge_prob) BB.embedchance = 5 else BB.damage = BB.damage diff --git a/code/game/objects/structures/maneater.dm b/code/game/objects/structures/maneater.dm index 60c5b6c9f2c..cf0f48a3076 100644 --- a/code/game/objects/structures/maneater.dm +++ b/code/game/objects/structures/maneater.dm @@ -138,6 +138,8 @@ return if(L.buckling) return // Something else is buckling them, maybe another maneater even + if(!L.client) + return buckle_mob(L, TRUE, check_loc = FALSE) START_PROCESSING(SSobj, src) if(!HAS_TRAIT(L, TRAIT_NOPAIN)) diff --git a/code/game/objects/structures/traveltile.dm b/code/game/objects/structures/traveltile.dm index b9ba41357e9..d52ea8ad838 100644 --- a/code/game/objects/structures/traveltile.dm +++ b/code/game/objects/structures/traveltile.dm @@ -49,11 +49,27 @@ var/can_gain_by_walking = FALSE var/check_other_side = FALSE var/list/revealed_to = list() + var/area/cached_destination_area /obj/structure/fluff/traveltile/Initialize() GLOB.traveltiles += src hide_if_needed() . = ..() + return INITIALIZE_HINT_LATELOAD + + +/obj/structure/fluff/traveltile/LateInitialize() + . = ..() + // Find our paired portal and cache what area it's in + resolve_destination_area() + +/obj/structure/fluff/traveltile/proc/resolve_destination_area() + if(!aportalgoesto) + return + for(var/obj/structure/fluff/traveltile/other in GLOB.traveltiles) // or however you iterate portals + if(other.aportalid == aportalgoesto) + cached_destination_area = get_area(other) + return /obj/structure/fluff/traveltile/Destroy() GLOB.traveltiles -= src diff --git a/code/modules/ambush/_ambush_config.dm b/code/modules/ambush/_ambush_config.dm new file mode 100644 index 00000000000..4a81a831946 --- /dev/null +++ b/code/modules/ambush/_ambush_config.dm @@ -0,0 +1,2 @@ +/datum/ambush_config + var/list/mob_types = list() diff --git a/code/modules/ambush/bog_ambush.dm b/code/modules/ambush/bog_ambush.dm new file mode 100644 index 00000000000..5bbcbcc72f2 --- /dev/null +++ b/code/modules/ambush/bog_ambush.dm @@ -0,0 +1,34 @@ +/datum/ambush_config/bog_guard_deserters + mob_types = list( + /mob/living/carbon/human/species/human/northern/bog_deserters/ambush = 2, + /mob/living/carbon/human/species/human/northern/bog_deserters/better_gear/ambush = 1 + ) + +/datum/ambush_config/bog_guard_deserters/hard + mob_types = list( + /mob/living/carbon/human/species/human/northern/bog_deserters/better_gear/ambush = 2, + /mob/living/carbon/human/species/human/northern/bog_deserters/ambush = 1, + ) + +/datum/ambush_config/mirespiders_ambush + mob_types = list( + /mob/living/simple_animal/hostile/retaliate/mirespider = 2, + /mob/living/simple_animal/hostile/mirespider_lurker = 1 + ) + +/datum/ambush_config/mirespiders_crawlers + mob_types = list( + /mob/living/simple_animal/hostile/retaliate/mirespider = 4, + ) + +/datum/ambush_config/mirespiders_aragn + mob_types = list( + /mob/living/simple_animal/hostile/retaliate/mirespider = 2, + /mob/living/simple_animal/hostile/mirespider_paralytic = 1 + ) + +/datum/ambush_config/mirespiders_unfair + mob_types = list( + /mob/living/simple_animal/hostile/mirespider_paralytic = 2, + /mob/living/simple_animal/hostile/mirespider_lurker = 1 + ) diff --git a/code/modules/ambush/coast.dm b/code/modules/ambush/coast.dm new file mode 100644 index 00000000000..37627e4245d --- /dev/null +++ b/code/modules/ambush/coast.dm @@ -0,0 +1,21 @@ +/datum/ambush_config/triple_deepone + mob_types = list( + /mob/living/simple_animal/hostile/deepone = 3 + ) + +/datum/ambush_config/deepone_party + mob_types = list( + /mob/living/simple_animal/hostile/deepone = 1, + /mob/living/simple_animal/hostile/deepone/spit = 1, + /mob/living/simple_animal/hostile/deepone/wiz = 1 + ) + +/datum/ambush_config/singular_minotaur + mob_types = list( + /mob/living/simple_animal/hostile/retaliate/minotaur = 1 + ) + +/datum/ambush_config/duo_minotaur + mob_types = list( + /mob/living/simple_animal/hostile/retaliate/minotaur = 2 + ) diff --git a/code/modules/ambush/mount_decap.dm b/code/modules/ambush/mount_decap.dm new file mode 100644 index 00000000000..c4a02ad2ab5 --- /dev/null +++ b/code/modules/ambush/mount_decap.dm @@ -0,0 +1,30 @@ +/datum/ambush_config/pair_of_direbear + mob_types = list( + /mob/living/simple_animal/hostile/retaliate/direbear = 2 + ) + +/datum/ambush_config/trio_of_highwaymen + mob_types = list( + /mob/living/carbon/human/species/human/northern/highwayman/ambush = 3 + ) + +/datum/ambush_config/singular_minotaur + mob_types = list( + /mob/living/simple_animal/hostile/retaliate/minotaur = 1 + ) + +/datum/ambush_config/duo_minotaur + mob_types = list( + /mob/living/simple_animal/hostile/retaliate/minotaur = 2 + ) + +/datum/ambush_config/medium_skeleton_party + mob_types = list( + /mob/living/carbon/human/species/skeleton/npc/medium = 3 + ) + +/datum/ambush_config/heavy_skeleton_party + mob_types = list( + /mob/living/carbon/human/species/skeleton/npc/medium = 1, + /mob/living/carbon/human/species/skeleton/npc/hard = 2 + ) diff --git a/code/modules/ambush/treasure_hunters.dm b/code/modules/ambush/treasure_hunters.dm new file mode 100644 index 00000000000..f0c7a825098 --- /dev/null +++ b/code/modules/ambush/treasure_hunters.dm @@ -0,0 +1,14 @@ +/datum/ambush_config/solo_treasure_hunter + mob_types = list( + /mob/living/carbon/human/species/human/northern/mad_touched_treasure_hunter/ambush = 1, + ) + +/datum/ambush_config/duo_treasure_hunter + mob_types = list( + /mob/living/carbon/human/species/human/northern/mad_touched_treasure_hunter/ambush = 2, + ) + +/datum/ambush_config/treasure_hunter_posse + mob_types = list( + /mob/living/carbon/human/species/human/northern/mad_touched_treasure_hunter/ambush = 3, + ) diff --git a/code/modules/ambush/wilderness.dm b/code/modules/ambush/wilderness.dm new file mode 100644 index 00000000000..52491fb2a4b --- /dev/null +++ b/code/modules/ambush/wilderness.dm @@ -0,0 +1,56 @@ +/datum/ambush_config/wolf_pack + mob_types = list( + /mob/living/simple_animal/hostile/retaliate/wolf = 3 + ) + +/datum/ambush_config/lone_troll + mob_types = list( + /mob/living/simple_animal/hostile/retaliate/troll/axe = 1 + ) + +/datum/ambush_config/troll_and_wolves + mob_types = list( + /mob/living/simple_animal/hostile/retaliate/troll/axe = 1, + /mob/living/simple_animal/hostile/retaliate/wolf = 2 + ) + +/datum/ambush_config/goblin_ambush_party + mob_types = list( + /mob/living/carbon/human/species/goblin/npc/ambush = 3 + ) + +/datum/ambush_config/goblin_raid_party + mob_types = list( + /mob/living/carbon/human/species/goblin/npc/ambush = 5 + ) + +/datum/ambush_config/raccoon_swarm + mob_types = list( + /mob/living/simple_animal/hostile/retaliate/raccoon = 4 + ) + +/datum/ambush_config/mole_pack + mob_types = list( + /mob/living/simple_animal/hostile/retaliate/mole = 3 + ) + +/datum/ambush_config/deserter_patrol + mob_types = list( + /mob/living/carbon/human/species/human/northern/militia/deserter = 3 + ) + +/datum/ambush_config/highwayman_duo + mob_types = list( + /mob/living/carbon/human/species/human/northern/highwayman/ambush = 2 + ) + +/datum/ambush_config/highwayman_gang + mob_types = list( + /mob/living/carbon/human/species/human/northern/highwayman/ambush = 4 + ) + +/datum/ambush_config/mixed_wildlife + mob_types = list( + /mob/living/simple_animal/hostile/retaliate/wolf = 2, + /mob/living/simple_animal/hostile/retaliate/raccoon = 2 + ) diff --git a/code/modules/clothing/belt/misc.dm b/code/modules/clothing/belt/misc.dm index 8b05a2282b3..55c84f4d255 100644 --- a/code/modules/clothing/belt/misc.dm +++ b/code/modules/clothing/belt/misc.dm @@ -176,6 +176,18 @@ grid_height = 64 grid_width = 32 +/obj/item/storage/belt/pouch/medicine + populate_contents = list( + /obj/item/needle, + /obj/item/natural/bundle/cloth/bandage/full, + /obj/item/reagent_containers/glass/bottle/healthpot + ) + +/obj/item/storage/belt/pouch/food + populate_contents = list( + /obj/item/reagent_containers/food/snacks/hardtack, + ) + /obj/item/storage/belt/pouch/coins/mid/Initialize() . = ..() var/obj/item/coin/silver/pile/H = new(loc) diff --git a/code/modules/clothing/quivers/misc.dm b/code/modules/clothing/quivers/misc.dm index d0c4841d537..c007bf27291 100644 --- a/code/modules/clothing/quivers/misc.dm +++ b/code/modules/clothing/quivers/misc.dm @@ -5,6 +5,7 @@ slot_flags = ITEM_SLOT_HIP|ITEM_SLOT_BACK max_storage = 20 ammo_type = list (/obj/item/ammo_casing/caseless/arrow, /obj/item/ammo_casing/caseless/bolt) + flags_ai_inventory = AI_ITEM_QUIVER /obj/item/ammo_holder/quiver/arrows fill_type = /obj/item/ammo_casing/caseless/arrow @@ -48,7 +49,7 @@ slot_flags = ITEM_SLOT_HIP|ITEM_SLOT_NECK max_storage = 10 ammo_type = list(/obj/item/ammo_casing/caseless/bullet) - + /obj/item/ammo_holder/bullet/bullets fill_type = /obj/item/ammo_casing/caseless/bullet diff --git a/code/modules/crafting/anvil_recipes/armor.dm b/code/modules/crafting/anvil_recipes/armor.dm index 8ef6310d529..75804e2c539 100644 --- a/code/modules/crafting/anvil_recipes/armor.dm +++ b/code/modules/crafting/anvil_recipes/armor.dm @@ -756,6 +756,18 @@ created_item = (/obj/item/clothing/head/helmet/visored/hounskull) craftdiff = 4 +/datum/anvil_recipe/armor/steel/barding + name = "Saiga Barding, Chainmail (+1 Steel)" + req_bar = /obj/item/ingot/steel + additional_items = list(/obj/item/ingot/steel) + created_item = /obj/item/clothing/barding/chain + +/datum/anvil_recipe/armor/steel/barding/honse + name = "Honse Barding, Chainmail (+1 Steel)" + req_bar = /obj/item/ingot/steel + additional_items = list(/obj/item/ingot/steel) + created_item = /obj/item/clothing/barding/honse/chain + /* /datum/anvil_recipe/armor/steel/warden_helm name = "Warden Helmet (+Bar)" diff --git a/code/modules/crafting/quality_of_crafting/sewing.dm b/code/modules/crafting/quality_of_crafting/sewing.dm index d452cb8fe51..ce844b0686c 100644 --- a/code/modules/crafting/quality_of_crafting/sewing.dm +++ b/code/modules/crafting/quality_of_crafting/sewing.dm @@ -1731,3 +1731,39 @@ ) craftdiff = 2 category = "Gloves" + +/datum/repeatable_crafting_recipe/sewing/barding + name = "padded barding (saiga)" + category = "Armor" + output = /obj/item/clothing/barding + requirements = list(/obj/item/natural/cloth = 4, + /obj/item/natural/fibers = 1) + craftdiff = 3 + +/datum/repeatable_crafting_recipe/sewing/barding/honse + name = "padded barding (honse)" + output = /obj/item/clothing/barding/honse + +/datum/repeatable_crafting_recipe/sewing/caparison + name = "caparison" + category = "Armor" + output =/obj/item/caparison + requirements = list(/obj/item/natural/cloth = 4, + /obj/item/natural/fibers = 2) + craftdiff = 2 + +/datum/repeatable_crafting_recipe/sewing/caparison/psy + name = "psydonite caparison" + output =/obj/item/caparison/psy + +/datum/repeatable_crafting_recipe/sewing/caparison/astrata + name = "astratan caparison" + output =/obj/item/caparison/astrata + +/datum/repeatable_crafting_recipe/sewing/caparison/eora + name = "eoran caparison" + output =/obj/item/caparison/eora + +/datum/repeatable_crafting_recipe/sewing/caparison/honse + name = "honse caparison" + output =/obj/item/caparison/honse diff --git a/code/modules/farming/items/eggs.dm b/code/modules/farming/items/eggs.dm index 06c8a668119..74ce72c25ef 100644 --- a/code/modules/farming/items/eggs.dm +++ b/code/modules/farming/items/eggs.dm @@ -31,8 +31,10 @@ visible_message("[H] crushes [src] underfoot.") qdel(src) -/obj/item/reagent_containers/food/snacks/egg/proc/hatch(mob/living/simple_animal/hostile/retaliate/chicken/parent) +/obj/item/reagent_containers/food/snacks/egg/proc/hatch(mob/living/simple_animal/hostile/retaliate/chicken/parent, mob/living/simple_animal/hostile/retaliate/chicken/father) record_round_statistic(STATS_ANIMALS_BRED) var/mob/living/simple_animal/hostile/retaliate/chicken/chick/new_chick = new /mob/living/simple_animal/hostile/retaliate/chicken/chick(get_turf(parent)) SEND_SIGNAL(parent, COMSIG_FRIENDSHIP_PASS_FRIENDSHIP, new_chick) SEND_SIGNAL(parent, COMSIG_HAPPINESS_PASS_HAPPINESS, new_chick) + if(parent.genetics && !ispath(parent.genetics)) + parent.genetics.inherit_to(new_chick, father) diff --git a/code/modules/food_and_drinks/rations.dm b/code/modules/food_and_drinks/rations.dm new file mode 100644 index 00000000000..e489c2d63c3 --- /dev/null +++ b/code/modules/food_and_drinks/rations.dm @@ -0,0 +1,54 @@ +// Design wise, I want to gate this behind high cooking skills role who can wrap their food and provide a convenient source of +// Stats-boosting food for adventurers / other alike. Currently there's very few good preserved food items that also give foodbuffs. +// This is a flavorful way to provide more reasons for adventurers to buy from innkeep instead of doing their own raisins. +// FOR GODS SAKE DO NOT GIVE THIS ROUNDSTART TO ANYONE BUT INNKEEP ON MAP. +/obj/item/ration // Ration. Sprites and Concept by Pintlewaiver. + name = "ration wrapping paper" + desc = "A piece of paper greased with a thin layer of oil, used to wrap food and preserve it for a long journey. \ + The final size of the ration depends on the size of the original food item. The food will last as long as the ration is wrapped." + icon = 'icons/obj/ration.dmi' + icon_state = "ration_wrapper" + w_class = WEIGHT_CLASS_TINY + grid_height = 32 + grid_width = 32 + dropshrink = 0.6 + var/obj/item/reagent_containers/food/snacks/food = null // The food item wrapped in the ration + +/obj/item/ration/attackby(obj/item/I, mob/user) + . = ..() + if(istype(I, /obj/item/reagent_containers/food/snacks)) + var/obj/item/reagent_containers/food/snacks/F = I + if(food) + to_chat(user, span_warning("There is already something wrapped in [src].")) + return + if(do_after(user, 2 SECONDS, target = src)) + user.transferItemToLoc(F, src) + food = I + to_chat(user, span_notice("You wrap [F] in the ration wrapper.")) + playsound(get_turf(user), 'sound/foley/dropsound/food_drop.ogg', 40, TRUE, -1) + F.rotprocess = null + if(I.w_class >= WEIGHT_CLASS_NORMAL) + name = "large ration pack ([food.name])" + desc = "A large ration pack containing a [food.name]." + icon_state = "ration_large" + dropshrink = 1 + // No need to change grid size cuz cakes are 1x1 huh?? + else + name = "small ration pack ([food.name])" + desc = "A small ration pack containing a [food.name]." + icon_state = "ration_small" + dropshrink = 1 + update_icon() + +/obj/item/ration/attack_self(mob/user) + . = ..() + if(food) + if(do_after(user, 2 SECONDS, target = src)) + to_chat(user, span_notice("You unwrap [food] from the ration wrapper.")) + playsound(get_turf(user), 'sound/foley/dropsound/food_drop.ogg', 40, TRUE, -1) + var/obj/item/reagent_containers/food/snacks/F = food + user.put_in_hands(F) + F.update_icon() + F.rotprocess = initial(F.rotprocess) + food = null + qdel(src) // No reusing wrapper diff --git a/code/modules/jobs/job_types/_job.dm b/code/modules/jobs/job_types/_job.dm index b010b6e05d9..549125d8a9d 100644 --- a/code/modules/jobs/job_types/_job.dm +++ b/code/modules/jobs/job_types/_job.dm @@ -11,6 +11,10 @@ var/datum/job/parent_job /// When joining the round, this text will be shown to the player. var/tutorial = null + /// Whether this job is intended to give quests + var/is_quest_giver = FALSE + /// How many quests this job can take at once + var/max_active_quests = 3 /// Id for the Job. var/id //Bitflags for the job diff --git a/code/modules/jobs/job_types/apprentices/shophand.dm b/code/modules/jobs/job_types/apprentices/shophand.dm index a697ddacbbd..b159a2b8cb5 100644 --- a/code/modules/jobs/job_types/apprentices/shophand.dm +++ b/code/modules/jobs/job_types/apprentices/shophand.dm @@ -10,6 +10,7 @@ total_positions = 1 spawn_positions = 1 display_order = JDO_SHOPHAND + is_quest_giver = TRUE give_bank_account = 10 bypass_lastclass = TRUE can_have_apprentices = FALSE diff --git a/code/modules/jobs/job_types/nobility/steward.dm b/code/modules/jobs/job_types/nobility/steward.dm index 7652226dc44..bcad824bd9f 100644 --- a/code/modules/jobs/job_types/nobility/steward.dm +++ b/code/modules/jobs/job_types/nobility/steward.dm @@ -11,6 +11,7 @@ total_positions = 1 spawn_positions = 1 bypass_lastclass = TRUE + is_quest_giver = TRUE allowed_races = RACES_PLAYER_NONDISCRIMINATED blacklisted_species = list(SPEC_ID_HALFLING) outfit = /datum/outfit/steward diff --git a/code/modules/mob/inventory.dm b/code/modules/mob/inventory.dm index b121c80a8cb..b01a9dd8699 100644 --- a/code/modules/mob/inventory.dm +++ b/code/modules/mob/inventory.dm @@ -5,6 +5,8 @@ /mob/proc/get_active_held_item() return get_item_for_held_index(active_hand_index) +/mob/proc/get_active_held_items() + return list(get_item_for_held_index(active_hand_index), get_item_for_held_index(get_inactive_hand_index())) //Finds the opposite limb for the active one (eg: upper left arm will find the item in upper right arm) //So we're treating each "pair" of limbs as a team, so "both" refers to them diff --git a/code/modules/mob/living/ambush.dm b/code/modules/mob/living/ambush.dm index d3fb0cacdc5..263ad90834f 100644 --- a/code/modules/mob/living/ambush.dm +++ b/code/modules/mob/living/ambush.dm @@ -1,7 +1,8 @@ // Mobs shouldn't consider their own ambush. // This should be it's own system. Please. -#define AMBUSH_CHANCE 5 +GLOBAL_VAR_INIT(ambush_chance_pct, 20) // Please don't raise this over 100 admins :') +GLOBAL_VAR_INIT(ambush_mobconsider_cooldown, 2 MINUTES) // Cooldown for each individual mob being considered for an ambush /mob/living/proc/ambushable() if(!mind) @@ -14,78 +15,173 @@ return FALSE return ambushable && !HAS_TRAIT(src, TRAIT_NOAMBUSH) -/mob/living/proc/consider_ambush() - if(!prob(AMBUSH_CHANCE)) - return - if(!MOBTIMER_FINISHED(src, MT_AMBUSHCHECK, 15 SECONDS)) - return - MOBTIMER_SET(src, MT_AMBUSHCHECK) - - if(!ambushable()) - return +/mob/living/proc/consider_ambush(always = FALSE, ignore_cooldown = FALSE, min_dist = 1, max_dist = 7) var/area/AR = get_area(src) - if(!length(AR.ambush_mobs)) + var/datum/threat_region/TR = SSregionthreat.get_region(AR.threat_region) + var/danger_level = DANGER_LEVEL_MODERATE // Fallback if there's no region + if(TR) + danger_level = TR.get_danger_level() + if(danger_level == DANGER_LEVEL_SAFE) + if(TR.latent_ambush == 0) + return + if(TR.latent_ambush <= DANGER_SAFE_LIMIT && !always) // Signal horn can dip below 10 + return + if(TR && ((world.time - TR.last_natural_ambush_time + 1 MINUTES) < 1 MINUTES)) return - var/turf/T = get_turf(src) - if(!T) + var/true_ambush_chance = GLOB.ambush_chance_pct + if(TR) + if(danger_level == DANGER_LEVEL_LOW) + true_ambush_chance *= 0.5 + else if(danger_level == DANGER_LEVEL_DANGEROUS) + true_ambush_chance *= 1.5 + else if(danger_level == DANGER_LEVEL_BLEAK) + true_ambush_chance *= 2 + if(!always && prob(100 - true_ambush_chance)) return - if(!(T.type in AR.ambush_types)) + if(get_will_block_ambush(src)) return - for(var/obj/machinery/light/fueled/RF in view(5, src)) - if(RF.on) + if(mob_timers["ambush_check"] && !ignore_cooldown) + if(world.time < mob_timers["ambush_check"] + GLOB.ambush_mobconsider_cooldown) return + mob_timers["ambush_check"] = world.time var/victims = 1 - var/list/victims_list + var/list/victimsa = list() for(var/mob/living/V in view(5, src)) if(V != src) if(V.ambushable()) victims++ - LAZYADD(victims_list, V) + victimsa += V if(victims > 3) return - var/static/list/valid_targets = list( - /obj/structure/flora/tree, \ - /obj/structure/flora/shroom_tree, \ - /obj/structure/flora/newtree - ) - var/list/possible_targets - for(var/obj/structure/object in oview(5, src)) //do not count the player - if(is_type_in_list(object, valid_targets)) - LAZYADD(possible_targets, get_turf(object)) - if(!LAZYLEN(possible_targets)) - return - MOBTIMER_SET(src, MT_AMBUSHLAST) - for(var/mob/living/V as anything in victims_list) - MOBTIMER_SET(V, MT_AMBUSHLAST) - var/spawnedtype = pickweight(AR.ambush_mobs) - var/mustype = 1 - for(var/i in 1 to clamp(victims, 2, 3)) - var/spawnloc = safepick(possible_targets) - if(!spawnloc) - return - var/mob/spawnedmob = new spawnedtype(spawnloc) - if(istype(spawnedmob, /mob/living/simple_animal/hostile)) - var/mob/living/simple_animal/hostile/M = spawnedmob - M.del_on_deaggro = 44 SECONDS - M.ai_controller?.set_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET, src) - if(istype(spawnedmob, /mob/living/carbon/human)) - var/mob/living/carbon/human/H = spawnedmob - H.del_on_deaggro = 44 SECONDS - H.last_aggro_loss = world.time - H.ai_controller?.set_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET, src) - mustype = 2 + var/list/possible_targets = get_possible_ambush_spawn(min_dist, max_dist) + if(possible_targets.len) + mob_timers["ambushlast"] = world.time + for(var/mob/living/V in victimsa) + V.mob_timers["ambushlast"] = world.time + if(TR) + TR.reduce_latent_ambush(1) // Remove one ambush from the ambient pool + TR.last_natural_ambush_time = world.time + var/list/mobs_to_spawn = list() + var/mobs_to_spawn_single = FALSE + var/max_spawns = 3 + var/mustype = 1 + var/spawnedtype = pickweight(AR.ambush_mobs) + + // This is the part where we scale ambush difficulty based on threat. Due to how we have a mix of + // Ambush Config and Single Mob Ambush, I use a weird scaling system: + // Single Mob + // Low - 1 Mob only + // Moderate - 1 to 2 (This is REALLY moderate) + // Dangerous - 2 to 3 + // Dire - 3 to 4 + // Ambush Difficulty Scaling: + // Low = -1 Mob + // Dangerous = +1 Mob + // Dire = + 2 Mobs + // Previous ambush system is 2 mobs, unless there's 3 victims, in which 3 mobs + // And Ambush Config number is fixed + + if(ispath(spawnedtype, /mob/living)) + switch(danger_level) + if(DANGER_LEVEL_SAFE) // Induced Ambush + max_spawns = 1 + if(DANGER_LEVEL_LOW) + max_spawns = 1 + if(DANGER_LEVEL_MODERATE) + max_spawns = rand(1, 2) // This is lower than before, to make moderate easier to deal with + if(DANGER_LEVEL_DANGEROUS) + max_spawns = rand(2, 3) + if(DANGER_LEVEL_BLEAK) + max_spawns = rand(3, 4) + mobs_to_spawn_single = TRUE + else if(istype(spawnedtype, /datum/ambush_config)) + var/datum/ambush_config/A = spawnedtype + for(var/type_path in A.mob_types) + var/amt = A.mob_types[type_path] + for(var/i in 1 to amt) + mobs_to_spawn += type_path + if(mobs_to_spawn.len > 1) + switch(danger_level) + if(DANGER_LEVEL_SAFE) + var/ri = rand(1, mobs_to_spawn.len) + mobs_to_spawn.Cut(ri, ri + 1) // Randomly remove one mob + if(DANGER_LEVEL_LOW) + var/ri = rand(1, mobs_to_spawn.len) + mobs_to_spawn.Cut(ri, ri + 1) // Randomly remove one mob + if(DANGER_LEVEL_DANGEROUS) + mobs_to_spawn += pick(mobs_to_spawn) // Randomly add 1 + if(DANGER_LEVEL_BLEAK) + mobs_to_spawn += pick(mobs_to_spawn) // Randomly add 2 + mobs_to_spawn += pick(mobs_to_spawn) + max_spawns = mobs_to_spawn.len + + for(var/i in 1 to max_spawns) + var/spawnloc = pick(possible_targets) + if(spawnloc) + var/mob_type + if(mobs_to_spawn_single) + mob_type = spawnedtype + else + if(!mobs_to_spawn.len) + continue + mob_type = mobs_to_spawn[1] + var/mob/spawnedmob = new mob_type(spawnloc) + if(mobs_to_spawn.len && !mobs_to_spawn_single) + mobs_to_spawn.Cut(1, 2) + if(istype(spawnedmob, /mob/living/simple_animal/hostile)) + var/mob/living/simple_animal/hostile/M = spawnedmob + M.del_on_deaggro = 44 SECONDS + M.faction += "ambush" + if(istype(spawnedmob, /mob/living/carbon/human)) + var/mob/living/carbon/human/H = spawnedmob + H.del_on_deaggro = 44 SECONDS + H.last_aggro_loss = world.time + H.faction += "ambush" + addtimer(CALLBACK(H, PROC_REF(setup_equip_block)), 3 SECONDS) + mustype = 2 + if(mustype == 1) + playsound_local(src, pick('sound/misc/jumpscare (1).ogg','sound/misc/jumpscare (2).ogg','sound/misc/jumpscare (3).ogg','sound/misc/jumpscare (4).ogg'), 100) + else + playsound_local(src, pick('sound/misc/jumphumans (1).ogg','sound/misc/jumphumans (2).ogg','sound/misc/jumphumans (3).ogg'), 100) + shake_camera(src, 2, 2) + +/mob/living/proc/setup_equip_block() + for(var/obj/item/clothing/clothing in contents) + clothing.AddElement(/datum/element/faction_restricted_equip) + +// Return whether a mob is blocked from being ambushed +/mob/living/proc/get_will_block_ambush() + if(!ambushable()) + return TRUE + var/campfires = 0 + for(var/obj/machinery/light/RF in view(5, src)) + if(RF.on) + campfires++ + if(campfires > 0) + return TRUE + +/mob/living/proc/get_possible_ambush_spawn(min_dist = 2, max_dist = 7) + var/list/possible_targets = list() + for(var/obj/structure/flora/tree/RT in orange(max_dist, src)) + if(istype(RT,/obj/structure/flora/tree/stump)) + continue + if(isturf(RT.loc) && !get_dist(RT.loc, src) < min_dist) + possible_targets += get_adjacent_ambush_turfs(RT.loc) + for(var/obj/structure/flora/grass/bush/RB in orange(max_dist, src)) + if(isturf(RB.loc) && !get_dist(RB.loc, src) < min_dist) + possible_targets += get_adjacent_ambush_turfs(RB.loc) + for(var/obj/structure/flora/newtree/RS in orange(max_dist, src)) + if(!RS.density) + continue + if(isturf(RS.loc) && !get_dist(RS.loc, src) < min_dist) + possible_targets += get_adjacent_ambush_turfs(RS.loc) - if(iscarbon(src)) - var/mob/living/carbon/C = src - var/heart_value = 30 - if(HAS_TRAIT(C, TRAIT_WEAK_HEART)) - heart_value *= 0.5 - if(C.stress >= heart_value && (prob(50))) - C.heart_attack() - if(mustype == 1) - playsound(src, pick('sound/misc/jumpscare (1).ogg','sound/misc/jumpscare (2).ogg','sound/misc/jumpscare (3).ogg','sound/misc/jumpscare (4).ogg'), 100) - else - playsound(src, pick('sound/misc/jumphumans (1).ogg','sound/misc/jumphumans (2).ogg','sound/misc/jumphumans (3).ogg'), 100) - shake_camera(src, 2, 2) + return possible_targets -#undef AMBUSH_CHANCE +/proc/get_adjacent_ambush_turfs(turf/T) + var/list/adjacent = list() + for(var/turf/AT in get_adjacent_open_turfs(T)) + if(AT.density || T.LinkBlockedWithAccess(AT, null)) + continue + adjacent += AT + return adjacent diff --git a/code/modules/mob/living/carbon/carbon.dm b/code/modules/mob/living/carbon/carbon.dm index 7db171c9d55..9c88313322e 100644 --- a/code/modules/mob/living/carbon/carbon.dm +++ b/code/modules/mob/living/carbon/carbon.dm @@ -350,7 +350,7 @@ ExtinguishMob(TRUE) return -/mob/living/carbon/resist_restraints() +/mob/living/carbon/resist_restraints(instant = FALSE) var/obj/item/I = null var/type = 0 if(handcuffed) @@ -366,10 +366,10 @@ if(type == 2) changeNext_move(CLICK_CD_RANGE) last_special = world.time + CLICK_CD_RANGE - cuff_resist(I) + cuff_resist(I, instant = instant) -/mob/living/carbon/proc/cuff_resist(obj/item/I, breakouttime = 1 MINUTES, cuff_break = 0) +/mob/living/carbon/proc/cuff_resist(obj/item/I, breakouttime = 1 MINUTES, cuff_break = 0, instant = FALSE) if(I.item_flags & BEING_REMOVED) to_chat(src, span_warning("I'm already trying to get out of \the [I]\s!")) return @@ -380,6 +380,10 @@ breakouttime = I.breakouttime if(STASTR > 15 || (mind && mind.has_antag_datum(/datum/antagonist/zombie)) ) cuff_break = INSTANT_CUFFBREAK + + if(instant) + cuff_break = INSTANT_CUFFBREAK + if(!cuff_break) to_chat(src, span_notice("I try to get out of \the [I]\s...")) if(do_after(src, breakouttime, timed_action_flags = (IGNORE_HELD_ITEM))) diff --git a/code/modules/mob/living/carbon/human/human.dm b/code/modules/mob/living/carbon/human/human.dm index db26e4d5eef..56159db2023 100644 --- a/code/modules/mob/living/carbon/human/human.dm +++ b/code/modules/mob/living/carbon/human/human.dm @@ -390,7 +390,7 @@ else to_chat(C, "I feel a breath of fresh air... which is a sensation you don't recognise...") -/mob/living/carbon/human/cuff_resist(obj/item/I) +/mob/living/carbon/human/cuff_resist(obj/item/I, breakouttime = 1 MINUTES, cuff_break = 0, instant = FALSE) if(..()) dropItemToGround(I) @@ -414,11 +414,11 @@ remove_atom_colour(TEMPORARY_COLOUR_PRIORITY, "#000000") cut_overlay(MA) -/mob/living/carbon/human/resist_restraints() +/mob/living/carbon/human/resist_restraints(instant = FALSE) if(wear_armor && wear_armor.breakouttime) changeNext_move(CLICK_CD_BREAKOUT) last_special = world.time + CLICK_CD_BREAKOUT - cuff_resist(wear_armor) + cuff_resist(wear_armor, instant = instant) else ..() diff --git a/code/modules/mob/living/carbon/human/npc/_npc.dm b/code/modules/mob/living/carbon/human/npc/_npc.dm index 9f44d162c54..a464815ac05 100644 --- a/code/modules/mob/living/carbon/human/npc/_npc.dm +++ b/code/modules/mob/living/carbon/human/npc/_npc.dm @@ -72,7 +72,7 @@ if(A) dropItemToGround(A, TRUE) -/mob/living/carbon/human/resist_restraints() +/mob/living/carbon/human/resist_restraints(instant = FALSE) var/obj/item/I = null if(handcuffed) I = handcuffed @@ -81,7 +81,7 @@ if(I) changeNext_move(CLICK_CD_BREAKOUT) last_special = world.time + CLICK_CD_BREAKOUT - cuff_resist(I) + cuff_resist(I, instant = instant) // attack using a held weapon otherwise bite the enemy, then if we are angry there is a chance we might calm down a little /mob/living/carbon/human/proc/monkey_attack(mob/living/L) diff --git a/code/modules/mob/living/carbon/human/npc/human_soldiers/bog_deserters.dm b/code/modules/mob/living/carbon/human/npc/human_soldiers/bog_deserters.dm new file mode 100644 index 00000000000..a5fd2c01f7d --- /dev/null +++ b/code/modules/mob/living/carbon/human/npc/human_soldiers/bog_deserters.dm @@ -0,0 +1,263 @@ + +//After the bogfort fell to undead, the remaining guard who didn't flea turned to bandirty. Wellarmed and trained. +//These guys use alot of iron stuff with small amounts of steel mixed in, not really one for finetuned balance might be too hard or easy idk. Going off vibes atm +/datum/outfit/job/human/northern/bog_deserters/proc/add_random_deserter_cloak(mob/living/carbon/human/H) + var/random_deserter_cloak = rand(1,4) + switch(random_deserter_cloak) + if(1) + cloak = /obj/item/clothing/cloak/stabard/mercenary + if(2) + cloak = /obj/item/clothing/cloak/stabard/colored/dungeon + if(3) + cloak = /obj/item/clothing/armor/brigandine/coatplates + +/datum/outfit/job/human/northern/bog_deserters/proc/add_random_deserter_weapon(mob/living/carbon/human/H) + var/random_deserter_weapon = rand(1,3) + switch(random_deserter_weapon) + if(1) + r_hand = /obj/item/weapon/sword/iron + l_hand = /obj/item/weapon/shield/heater + if(2) + r_hand = /obj/item/weapon/polearm/spear + if(3) + r_hand = /obj/item/weapon/axe + +/datum/outfit/job/human/northern/bog_deserters/proc/add_random_deserter_weapon_hard(mob/living/carbon/human/H) + var/add_random_deserter_weapon_hard = rand(1,4) + switch(add_random_deserter_weapon_hard) + if(1) + r_hand = /obj/item/weapon/sword/iron + l_hand = /obj/item/weapon/shield/heater + if(2) + r_hand = /obj/item/weapon/mace/warhammer + l_hand = /obj/item/weapon/shield/heater + if(3) + r_hand = /obj/item/weapon/axe + if(4) + r_hand = /obj/item/weapon/flail + l_hand = /obj/item/weapon/shield/heater + +/datum/outfit/job/human/northern/bog_deserters/proc/add_random_deserter_beltl_stuff(mob/living/carbon/human/H) + var/add_random_deserter_beltl_stuff = rand(1,7) + switch(add_random_deserter_beltl_stuff) + if(1) + beltl = /obj/item/storage/belt/pouch/food + if(2) + beltl = /obj/item/storage/belt/pouch/medicine + if(3) + beltl = /obj/item/storage/belt/pouch/coins/poor + if(4) + beltl = /obj/item/storage/belt/pouch/coins/mid + if(5) + beltl = /obj/item/reagent_containers/glass/bottle/waterskin + if(6) + beltl = /obj/item/reagent_containers/glass/bottle/healthpot + if(7) + beltl = /obj/item/weapon/scabbard/sword + +/datum/outfit/job/human/northern/bog_deserters/proc/add_random_deserter_beltr_stuff(mob/living/carbon/human/H) + var/add_random_deserter_beltr_stuff = rand(1,7) + switch(add_random_deserter_beltr_stuff) + if(1) + beltr = /obj/item/storage/belt/pouch/food + if(2) + beltr = /obj/item/storage/belt/pouch/medicine + if(3) + beltr = /obj/item/storage/belt/pouch/coins/poor + if(4) + beltr = /obj/item/storage/belt/pouch/coins/mid + if(5) + beltr = /obj/item/reagent_containers/glass/bottle/waterskin + if(6) + beltr = /obj/item/reagent_containers/glass/bottle/healthpot + if(7) + beltr = /obj/item/weapon/scabbard/sword + +/datum/outfit/job/human/northern/bog_deserters/proc/add_random_deserter_armor_hard(mob/living/carbon/human/H) + var/random_deserter_armor_hard = rand(1,3) + switch(random_deserter_armor_hard) + if(1) + armor = /obj/item/clothing/armor/brigandine/light + if(2) + armor = /obj/item/clothing/armor/cuirass/iron + if(3) + armor = /obj/item/clothing/armor/plate/fluted + +/mob/living/carbon/human/species/human/northern/bog_deserters + ai_controller = /datum/ai_controller/human_npc + faction = list("viking", "station") + ambushable = FALSE + cmode = 1 + setparrytime = 30 + flee_in_pain = TRUE + a_intent = INTENT_HELP + d_intent = INTENT_PARRY + possible_mmb_intents = list(INTENT_BITE, INTENT_JUMP, INTENT_KICK) + possible_rmb_intents = list( + /datum/rmb_intent/feint,\ + /datum/rmb_intent/aimed,\ + /datum/rmb_intent/strong,\ + /datum/rmb_intent/riposte,\ + /datum/rmb_intent/weak + ) + var/is_silent = FALSE /// Determines whether or not we will scream our funny lines at people. + + +/mob/living/carbon/human/species/human/northern/bog_deserters/ambush + wander = TRUE + +/mob/living/carbon/human/species/human/northern/bog_deserters/Initialize() + . = ..() + AddComponent(/datum/component/ai_aggro_system) + set_species(/datum/species/human/northern) + addtimer(CALLBACK(src, PROC_REF(after_creation)), 1 SECONDS) + is_silent = TRUE + + +/mob/living/carbon/human/species/human/northern/bog_deserters/after_creation() + ..() + job = "Garrison Deserter" + ADD_TRAIT(src, TRAIT_NOMOOD, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_NOHUNGER, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_LEECHIMMUNE, INNATE_TRAIT) + ADD_TRAIT(src, TRAIT_BREADY, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_MEDIUMARMOR, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_KNEESTINGER_IMMUNITY, TRAIT_GENERIC) //For when they're just kinda patrolling around/ambushes + equipOutfit(new /datum/outfit/job/human/northern/bog_deserters) + var/obj/item/organ/eyes/organ_eyes = getorgan(/obj/item/organ/eyes) + if(organ_eyes) + organ_eyes.eye_color = pick("27becc", "35cc27", "000000") + update_body() + var/obj/item/bodypart/head/head = get_bodypart(BODY_ZONE_HEAD) + head.sellprice = 50 // Big sellprice for these guys since they're deserters + +/datum/outfit/job/human/northern/bog_deserters/pre_equip(mob/living/carbon/human/H) + ..() + //Body Stuff + H.set_eye_color("#27becc","#27becc") + H.set_hair_color("#61310f") + H.set_facial_hair_color(H.get_hair_color()) + if(H.gender == FEMALE) + H.set_hair_style(/datum/sprite_accessory/hair/head/messy) + else + H.set_hair_style(/datum/sprite_accessory/hair/head/messy) + H.set_facial_hair_style(/datum/sprite_accessory/hair/facial/manly) + //skill Stuff + H.adjust_skillrank(/datum/skill/combat/axesmaces, 4, TRUE) //NPCs do not get these skills unless a mind takes them over, hopefully in the future someone can fix + H.adjust_skillrank(/datum/skill/combat/whipsflails, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/polearms, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/swords, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/shields, 3, TRUE) + H.adjust_skillrank(/datum/skill/combat/wrestling, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/unarmed, 4, TRUE) + H.adjust_skillrank(/datum/skill/misc/athletics, 3, TRUE) + ADD_TRAIT(H, TRAIT_MEDIUMARMOR, TRAIT_GENERIC) + ADD_TRAIT(H, TRAIT_HEAVYARMOR, TRAIT_GENERIC) + ADD_TRAIT(H, TRAIT_STEELHEARTED, TRAIT_GENERIC) + H.base_strength = rand(12,14) + H.base_speed = 11 + H.base_constitution = rand(11,13) + H.base_endurance = 13 + H.base_perception = 11 + H.base_intelligence = 10 + //Chest Gear + add_random_deserter_cloak(H) + shirt = /obj/item/clothing/armor/gambeson + armor = /obj/item/clothing/armor/chainmail/hauberk/iron + //Head Gear + neck = /obj/item/clothing/neck/coif + head = /obj/item/clothing/head/helmet/kettle/iron + //wrist Gear + gloves = /obj/item/clothing/gloves/chain/iron + wrists = /obj/item/clothing/wrists/bracers/iron + //Lower Gear + belt = /obj/item/storage/belt/leather + pants = /obj/item/clothing/pants/chainlegs/iron + shoes = /obj/item/clothing/shoes/boots/armor + //Weapons + add_random_deserter_weapon(H) + add_random_deserter_beltl_stuff(H) + add_random_deserter_beltr_stuff(H) + +/mob/living/carbon/human/species/human/northern/bog_deserters/better_gear + faction = list("viking", "station") + ambushable = FALSE + cmode = 1 + setparrytime = 30 + flee_in_pain = TRUE + a_intent = INTENT_HELP + d_intent = INTENT_PARRY + possible_mmb_intents = list(INTENT_BITE, INTENT_JUMP, INTENT_KICK) + possible_rmb_intents = list( + /datum/rmb_intent/feint,\ + /datum/rmb_intent/aimed,\ + /datum/rmb_intent/strong,\ + /datum/rmb_intent/riposte,\ + /datum/rmb_intent/weak + ) + +/mob/living/carbon/human/species/human/northern/bog_deserters/better_gear/ambush + wander = TRUE + +/mob/living/carbon/human/species/human/northern/bog_deserters/better_gear/after_creation() + job = "Garrison Deserter" + ADD_TRAIT(src, TRAIT_NOMOOD, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_NOHUNGER, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_LEECHIMMUNE, INNATE_TRAIT) + ADD_TRAIT(src, TRAIT_BREADY, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_MEDIUMARMOR, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_KNEESTINGER_IMMUNITY, TRAIT_GENERIC) //For when they're just kinda patrolling around/ambushes + equipOutfit(new /datum/outfit/job/human/northern/bog_deserters/better_gear) + var/obj/item/organ/eyes/organ_eyes = getorgan(/obj/item/organ/eyes) + if(organ_eyes) + organ_eyes.eye_color = pick("27becc", "35cc27", "000000") + update_body() + var/obj/item/bodypart/head/head = get_bodypart(BODY_ZONE_HEAD) + head.sellprice = 50 // Big sellprice for these guys since they're deserters + +/datum/outfit/job/human/northern/bog_deserters/better_gear/pre_equip(mob/living/carbon/human/H) + //Body Stuff + H.set_eye_color("#27becc","#27becc") + H.set_hair_color("#61310f") + H.set_facial_hair_color(H.get_hair_color()) + if(H.gender == FEMALE) + H.set_hair_style(/datum/sprite_accessory/hair/head/messy) + else + H.set_hair_style(/datum/sprite_accessory/hair/head/messy) + H.set_facial_hair_style(/datum/sprite_accessory/hair/facial/manly) + //skill Stuff + H.adjust_skillrank(/datum/skill/combat/axesmaces, 4, TRUE) //NPCs do not get these skills unless a mind takes them over, hopefully in the future someone can fix + H.adjust_skillrank(/datum/skill/combat/whipsflails, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/polearms, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/swords, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/shields, 3, TRUE) + H.adjust_skillrank(/datum/skill/combat/wrestling, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/unarmed, 4, TRUE) + H.adjust_skillrank(/datum/skill/misc/athletics, 3, TRUE) + ADD_TRAIT(H, TRAIT_MEDIUMARMOR, TRAIT_GENERIC) + ADD_TRAIT(H, TRAIT_HEAVYARMOR, TRAIT_GENERIC) + ADD_TRAIT(H, TRAIT_STEELHEARTED, TRAIT_GENERIC) + H.base_strength = rand(12,14) + H.base_speed = 11 + H.base_constitution = rand(11,13) + H.base_endurance = 13 + H.base_perception = 11 + H.base_intelligence = 10 + //Chest Gear + shirt = /obj/item/clothing/armor/chainmail/hauberk/iron + add_random_deserter_armor_hard(H) + add_random_deserter_cloak(H) + //Head Gear + neck = /obj/item/clothing/neck/chaincoif/iron + head = /obj/item/clothing/head/helmet/heavy/frog + //wrist Gear + gloves = /obj/item/clothing/gloves/plate/iron + wrists = /obj/item/clothing/wrists/bracers/iron + //Lower Gear + belt = /obj/item/storage/belt/leather + pants = /obj/item/clothing/pants/chainlegs/iron + shoes = /obj/item/clothing/shoes/boots/armor + //Weapons + add_random_deserter_weapon_hard(H) + add_random_deserter_beltl_stuff(H) + add_random_deserter_beltr_stuff(H) diff --git a/code/modules/mob/living/carbon/human/npc/human_soldiers/deranged.dm b/code/modules/mob/living/carbon/human/npc/human_soldiers/deranged.dm new file mode 100644 index 00000000000..c186f1cc1b5 --- /dev/null +++ b/code/modules/mob/living/carbon/human/npc/human_soldiers/deranged.dm @@ -0,0 +1,255 @@ +/* * + * Deranged Knight + * A miniboss for quest system, designed to be a high-level challenge for multiple players. + * Uses fuckoff gear that should not be looted - hence snowflake dismemberment code. + */ + +GLOBAL_LIST_INIT(matthios_aggro, file2list("strings/rt/matthiosaggrolines.txt")) +GLOBAL_LIST_INIT(zizo_aggro, file2list("strings/rt/zizoaggrolines.txt")) +GLOBAL_LIST_INIT(graggar_aggro, file2list("strings/rt/graggaraggrolines.txt")) +GLOBAL_LIST_INIT(hedgeknight_aggro, file2list("strings/rt/hedgeknightaggrolines.txt")) + +/mob/living/carbon/human/species/human/northern/deranged_knight + ai_controller = /datum/ai_controller/human_npc + faction = list("dundead") + ambushable = FALSE + dodgetime = 30 + flee_in_pain = TRUE + possible_rmb_intents = list( + /datum/rmb_intent/feint,\ + /datum/rmb_intent/aimed,\ + /datum/rmb_intent/strong,\ + /datum/rmb_intent/riposte,\ + /datum/rmb_intent/weak + ) + var/is_silent = FALSE /// Determines whether or not we will scream our funny lines at people. + var/preset = "matthios" + var/forced_preset = "" // If set, force a specific preset instead of randomizing. + +/mob/living/carbon/human/species/human/northern/deranged_knight/Initialize() + . = ..() + addtimer(CALLBACK(src, PROC_REF(after_creation)), 1 SECONDS) + AddComponent(/datum/component/ai_aggro_system) + is_silent = TRUE + var/head = get_bodypart(BODY_ZONE_HEAD) + RegisterSignal(head, COMSIG_MOB_DISMEMBER, PROC_REF(handle_drop_limb)) + + +/mob/living/carbon/human/species/human/northern/deranged_knight/Destroy() + var/head = get_bodypart(BODY_ZONE_HEAD) + if(head) + UnregisterSignal(head, COMSIG_MOB_DISMEMBER) + return ..() + +/// Snowflake DK behavior for decaps. Yes, they turn to dust prior to decaps. +/mob/living/carbon/human/species/human/northern/deranged_knight/proc/handle_drop_limb(obj/item/bodypart/bodypart, special) + if(!istype(bodypart, /obj/item/bodypart/head)) + return + + death(FALSE, TRUE) // No, you won't loot that tasty helmet. + return COMPONENT_CANCEL_DISMEMBER + +/mob/living/carbon/human/species/human/northern/deranged_knight/after_creation() + ..() + job = "Ascendant Knight" + ADD_TRAIT(src, TRAIT_NOMOOD, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_NOHUNGER, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_LEECHIMMUNE, INNATE_TRAIT) + ADD_TRAIT(src, TRAIT_BREADY, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_HEAVYARMOR, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_STUCKITEMS, TRAIT_GENERIC) + if(forced_preset) + preset = forced_preset + else + switch(rand(1, 4)) + if(1) + preset = "graggar" + SEND_SIGNAL(src, COMSIG_MOB_MODIFY_AGGRO_LINES, GLOB.graggar_aggro, TRUE) + SEND_SIGNAL(src, COMSIG_MOB_MODIFY_DEATH_LINES, list("No more... Blood!"), TRUE) + if(2) + preset = "matthios" + SEND_SIGNAL(src, COMSIG_MOB_MODIFY_AGGRO_LINES, GLOB.matthios_aggro, TRUE) + SEND_SIGNAL(src, COMSIG_MOB_MODIFY_DEATH_LINES, list("Matthios, I have failed you...", "Matthios, is this true?!"), TRUE) + if(3) + preset = "zizo" + SEND_SIGNAL(src, COMSIG_MOB_MODIFY_AGGRO_LINES, GLOB.zizo_aggro, TRUE) + SEND_SIGNAL(src, COMSIG_MOB_MODIFY_DEATH_LINES, list("Zizo, forgive me!"), TRUE) + if(4) + preset = "hedgeknight" + SEND_SIGNAL(src, COMSIG_MOB_MODIFY_AGGRO_LINES, GLOB.hedgeknight_aggro, TRUE) + switch(preset) + if("graggar") + equipOutfit(new /datum/outfit/job/quest_miniboss/graggar) + if ("matthios") + equipOutfit(new /datum/outfit/job/quest_miniboss/matthios) + if ("zizo") + ADD_TRAIT(src, TRAIT_CABAL, TRAIT_GENERIC) + equipOutfit(new /datum/outfit/job/quest_miniboss/zizo) + if ("hedgeknight") + if(prob(50)) + equipOutfit(new /datum/outfit/job/quest_miniboss/hedge_knight) + else + equipOutfit(new /datum/outfit/job/quest_miniboss/blacksteel) + // No special trait for hedgeknight, he's just a generic tough guy. + + gender = pick(MALE,FEMALE) + regenerate_icons() + + var/obj/item/organ/eyes/organ_eyes = getorgan(/obj/item/organ/eyes) + var/obj/item/organ/ears/organ_ears = getorgan(/obj/item/organ/ears) + var/obj/item/bodypart/head/head = get_bodypart(BODY_ZONE_HEAD) + var/hairf = pick(list( + /datum/sprite_accessory/hair/head/countryponytailalt, + /datum/sprite_accessory/hair/head/stacy, + /datum/sprite_accessory/hair/head/kusanagi_alt)) + var/hairm = pick(list(/datum/sprite_accessory/hair/head/ponytailwitcher, + /datum/sprite_accessory/hair/head/dave, + /datum/sprite_accessory/hair/head/sabitsuki)) + + var/datum/bodypart_feature/hair/head/new_hair = new() + + if(gender == FEMALE) + new_hair.set_accessory_type(hairf, null, src) + else + new_hair.set_accessory_type(hairm, null, src) + + new_hair.accessory_colors = "#DDDDDD" + new_hair.hair_color = "#DDDDDD" + set_hair_color("#DDDDDD") + + head.add_bodypart_feature(new_hair) + + dna.update_ui_block(DNA_HAIR_COLOR_BLOCK) + dna.species.handle_body(src) + + if(organ_eyes) + organ_eyes.eye_color = "#FFBF00" + organ_eyes.accessory_colors = "#FFBF00#FFBF00" + + if(organ_ears) + organ_ears.accessory_colors = "#5f5f70" + + skin_tone = "5f5f70" + + if(prob(1)) + real_name = "Taras Mura" + update_body() + + def_intent_change(INTENT_PARRY) + +/mob/living/carbon/human/species/human/northern/deranged_knight/death(gibbed, nocutscene) + . = ..() + if(!gibbed) + dust(FALSE, FALSE, TRUE) + +/datum/outfit/job/quest_miniboss/pre_equip(mob/living/carbon/human/H, visualsOnly) + . = ..() + H.base_strength = 15 + H.base_speed = 14 + H.base_constitution = 15 + H.base_endurance = 14 + H.base_perception = 12 + H.base_intelligence = 12 + H.base_fortune = 10 + + H.adjust_skillrank(/datum/skill/combat/whipsflails, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/polearms, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/axesmaces, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/swords, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/shields, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/unarmed, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/wrestling, 4, TRUE) + H.adjust_skillrank(/datum/skill/misc/swimming, 2, TRUE) + H.adjust_skillrank(/datum/skill/misc/climbing, 2, TRUE) + +/datum/outfit/job/quest_miniboss/matthios/pre_equip(mob/living/carbon/human/H) + . = ..() + + armor = /obj/item/clothing/armor/plate/full/matthios + pants = /obj/item/clothing/pants/platelegs/matthios + shoes = /obj/item/clothing/shoes/boots/armor/matthios + wrists = /obj/item/clothing/wrists/bracers + gloves = /obj/item/clothing/gloves/plate/matthios + head = /obj/item/clothing/head/helmet/heavy/matthios + neck = /obj/item/clothing/neck/gorget + r_hand = /obj/item/weapon/flail/peasantwarflail/matthios + mask = /obj/item/clothing/face/facemask/steel + +/datum/outfit/job/quest_miniboss/zizo/pre_equip(mob/living/carbon/human/H) + . = ..() + + armor = /obj/item/clothing/armor/plate/full/zizo + pants = /obj/item/clothing/pants/platelegs/zizo + shoes = /obj/item/clothing/shoes/boots/armor/zizo + wrists = /obj/item/clothing/wrists/bracers + gloves = /obj/item/clothing/gloves/plate/zizo + head = /obj/item/clothing/head/helmet/heavy/zizo + neck = /obj/item/clothing/neck/gorget + r_hand = /obj/item/weapon/sword/long + mask = /obj/item/clothing/face/facemask/steel + +/datum/outfit/job/quest_miniboss/graggar/pre_equip(mob/living/carbon/human/H) + . = ..() + + armor = /obj/item/clothing/armor/plate/fluted/ornate + pants = /obj/item/clothing/pants/platelegs/graggar + shoes = /obj/item/clothing/shoes/boots/armor/graggar + gloves = /obj/item/clothing/gloves/plate/graggar + wrists = /obj/item/clothing/wrists/bracers + head = /obj/item/clothing/head/helmet/heavy/graggar + neck = /obj/item/clothing/neck/gorget + r_hand = /obj/item/weapon/greataxe/steel/doublehead/graggar + mask = /obj/item/clothing/face/facemask/steel + wrists = /obj/item/clothing/wrists/bracers + cloak = /obj/item/clothing/cloak/graggar + +/datum/outfit/job/quest_miniboss/blacksteel/pre_equip(mob/living/carbon/human/H) + . = ..() + + armor = /obj/item/clothing/armor/plate/blkknight + pants = /obj/item/clothing/pants/platelegs/blk + shoes = /obj/item/clothing/shoes/boots/armor/blkknight + gloves = /obj/item/clothing/gloves/plate/blk + wrists = /obj/item/clothing/wrists/bracers + head = /obj/item/clothing/head/helmet/blacksteel + neck = /obj/item/clothing/neck/gorget + r_hand = /obj/item/weapon/sword/long/greatsword + mask = /obj/item/clothing/face/facemask/steel + wrists = /obj/item/clothing/wrists/bracers + +/datum/outfit/job/quest_miniboss/hedge_knight/pre_equip(mob/living/carbon/human/H) + . = ..() + + armor = /obj/item/clothing/armor/plate/fluted + pants = /obj/item/clothing/pants/platelegs + shoes = /obj/item/clothing/shoes/boots/armor + gloves = /obj/item/clothing/gloves/plate + head = /obj/item/clothing/head/helmet/heavy/frog + neck = /obj/item/clothing/neck/gorget + r_hand = /obj/item/weapon/sword/long/greatsword/gutsclaymore + mask = /obj/item/clothing/face/facemask/steel + belt = /obj/item/storage/belt/leather/steel + beltl = /obj/item/flashlight/flare/torch/lantern + beltr = /obj/item/weapon/sword/long + wrists = /obj/item/clothing/wrists/bracers + cloak = /obj/item/clothing/cloak/stabard/colored/dungeon + +/* + * Goon preset + * Intended to support knight, but should not have any special/overly expensive gear. +*/ + +/mob/living/carbon/human/species/human/northern/highwayman/dk_goon + faction = list("dundead") + +/mob/living/carbon/human/species/human/northern/deranged_knight/matthios + forced_preset = "matthios" + +/mob/living/carbon/human/species/human/northern/deranged_knight/zizo + forced_preset = "zizo" + +/mob/living/carbon/human/species/human/northern/deranged_knight/graggar + forced_preset = "graggar" + +/mob/living/carbon/human/species/human/northern/deranged_knight/hedgeknight + forced_preset = "hedgeknight" diff --git a/code/modules/mob/living/carbon/human/npc/human_soldiers/drow_raiders.dm b/code/modules/mob/living/carbon/human/npc/human_soldiers/drow_raiders.dm new file mode 100644 index 00000000000..e12cf1b5b3b --- /dev/null +++ b/code/modules/mob/living/carbon/human/npc/human_soldiers/drow_raiders.dm @@ -0,0 +1,117 @@ +GLOBAL_LIST_INIT(drowraider_aggro, file2list("strings/rt/drowaggrolines.txt")) + +/mob/living/carbon/human/species/elf/dark/drowraider + ai_controller = /datum/ai_controller/human_npc + faction = list("drow") + ambushable = FALSE + dodgetime = 30 + flee_in_pain = TRUE + d_intent = INTENT_DODGE + possible_rmb_intents = list() + var/is_silent = FALSE /// Determines whether or not we will scream our funny lines at people. + +/mob/living/carbon/human/species/elf/dark/drowraider/ambush + wander = TRUE + +/mob/living/carbon/human/species/elf/dark/drowraider/Initialize() + . = ..() + AddComponent(/datum/component/ai_aggro_system) + set_species(/datum/species/elf/dark) + addtimer(CALLBACK(src, PROC_REF(after_creation)), 1 SECONDS) + is_silent = TRUE + + +/mob/living/carbon/human/species/elf/dark/drowraider/after_creation() + ..() + job = "Drow Raider" + ADD_TRAIT(src, TRAIT_NOMOOD, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_NOHUNGER, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_LEECHIMMUNE, INNATE_TRAIT) + ADD_TRAIT(src, TRAIT_BREADY, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_HEAVYARMOR, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_DODGEEXPERT, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_DUALWIELDER, TRAIT_GENERIC) + equipOutfit(new /datum/outfit/job/human/species/elf/dark/drowraider) + if(prob(40)) + gender = MALE + else + gender = FEMALE + regenerate_icons() + + var/obj/item/organ/eyes/organ_eyes = getorgan(/obj/item/organ/eyes) + var/obj/item/organ/ears/organ_ears = getorgan(/obj/item/organ/ears) + var/obj/item/bodypart/head/head = get_bodypart(BODY_ZONE_HEAD) + var/hairf = pick(list( + /datum/sprite_accessory/hair/head/countryponytailalt, + /datum/sprite_accessory/hair/head/stacy, + /datum/sprite_accessory/hair/head/kusanagi_alt)) + var/hairm = pick(list(/datum/sprite_accessory/hair/head/ponytailwitcher, + /datum/sprite_accessory/hair/head/dave, + /datum/sprite_accessory/hair/head/sabitsuki)) + + var/datum/bodypart_feature/hair/head/new_hair = new() + + if(gender == FEMALE) + new_hair.set_accessory_type(hairf, null, src) + else + new_hair.set_accessory_type(hairm, null, src) + + new_hair.accessory_colors = "#DDDDDD" + new_hair.hair_color = "#DDDDDD" + set_hair_color("#DDDDDD") + + head.add_bodypart_feature(new_hair) + head.sellprice = 40 + + dna.update_ui_block(DNA_HAIR_COLOR_BLOCK) + dna.species.handle_body(src) + + if(organ_eyes) + organ_eyes.eye_color = "#FFBF00" + organ_eyes.accessory_colors = "#FFBF00#FFBF00" + + if(organ_ears) + organ_ears.accessory_colors = "#5f5f70" + + skin_tone = "5f5f70" + + if(gender == FEMALE) + real_name = pick(file2list("strings/rt/names/elf/elfdf.txt")) + else + real_name = pick(file2list("strings/rt/names/elf/elfdm.txt")) + + faction += "spider_lowers" + + update_body() + +/datum/outfit/job/human/species/elf/dark/drowraider/pre_equip(mob/living/carbon/human/H) + shoes = /obj/item/clothing/shoes/boots/leather/advanced + pants = /obj/item/clothing/pants/trou/shadowpants + armor = /obj/item/clothing/armor/leather/jacket/silk_coat + shirt = /obj/item/clothing/shirt/shadowshirt + gloves = /obj/item/clothing/gloves/fingerless/shadowgloves + wrists = /obj/item/clothing/wrists/bracers/leather/advanced + mask = /obj/item/clothing/face/facemask + neck = /obj/item/clothing/neck/coif + r_hand = /obj/item/weapon/whip + if(prob(45)) + r_hand = /obj/item/weapon/sword/sabre/stalker + l_hand = /obj/item/weapon/sword/sabre/stalker + else if(prob(15)) + r_hand = /obj/item/weapon/knife/dagger/steel/dirk + l_hand = /obj/item/weapon/knife/dagger/steel/dirk + + H.base_strength = 12 // 6 Points + H.base_speed = 13 // 3 points + H.base_constitution = 14 // 4 points + H.base_endurance = 12 // 2 points - 14 points spread. Equal to 1 more than a KC accounting for Statpack. + H.base_perception = 10 + H.base_intelligence = 10 + H.adjust_skillrank(/datum/skill/combat/whipsflails, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/axesmaces, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/swords, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/shields, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/unarmed, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/wrestling, 4, TRUE) + H.adjust_skillrank(/datum/skill/misc/swimming, 2, TRUE) + H.adjust_skillrank(/datum/skill/misc/climbing, 2, TRUE) diff --git a/code/modules/mob/living/carbon/human/npc/human_soldiers/highwaymen.dm b/code/modules/mob/living/carbon/human/npc/human_soldiers/highwaymen.dm new file mode 100644 index 00000000000..61f522752e6 --- /dev/null +++ b/code/modules/mob/living/carbon/human/npc/human_soldiers/highwaymen.dm @@ -0,0 +1,95 @@ +GLOBAL_LIST_INIT(highwayman_aggro, file2list("strings/rt/highwaymanaggrolines.txt")) + +/mob/living/carbon/human/species/human/northern/highwayman + ai_controller = /datum/ai_controller/human_npc + faction = list("viking", "station") + ambushable = FALSE + dodgetime = 30 + flee_in_pain = TRUE + d_intent = INTENT_PARRY + possible_rmb_intents = list() + var/is_silent = FALSE /// Determines whether or not we will scream our funny lines at people. + + +/mob/living/carbon/human/species/human/northern/highwayman/ambush + wander = TRUE + + +/mob/living/carbon/human/species/human/northern/highwayman/Initialize() + . = ..() + AddComponent(/datum/component/ai_aggro_system) + set_species(/datum/species/human/northern) + addtimer(CALLBACK(src, PROC_REF(after_creation)), 1 SECONDS) + is_silent = TRUE + + +/mob/living/carbon/human/species/human/northern/highwayman/after_creation() + ..() + job = "Highwayman" + ADD_TRAIT(src, TRAIT_NOMOOD, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_NOHUNGER, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_LEECHIMMUNE, INNATE_TRAIT) + ADD_TRAIT(src, TRAIT_BREADY, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_MEDIUMARMOR, TRAIT_GENERIC) + equipOutfit(new /datum/outfit/job/human/species/human/northern/highwayman) + var/obj/item/organ/eyes/organ_eyes = getorgan(/obj/item/organ/eyes) + if(organ_eyes) + organ_eyes.eye_color = pick("27becc", "35cc27", "000000") + update_body() + var/obj/item/bodypart/head/head = get_bodypart(BODY_ZONE_HEAD) + head.sellprice = 30 // 50% More than goblin + + +/datum/outfit/job/human/species/human/northern/highwayman/pre_equip(mob/living/carbon/human/H) + wrists = /obj/item/clothing/wrists/bracers/leather + if(prob(50)) + mask = /obj/item/clothing/face/shepherd/rag + armor = /obj/item/clothing/armor/leather + shirt = /obj/item/clothing/shirt/undershirt/colored/vagrant + if(prob(50)) + shirt = /obj/item/clothing/armor/gambeson/light + pants = /obj/item/clothing/pants/trou/leather + if(prob(50)) + head = /obj/item/clothing/head/helmet/leather + if(prob(30)) + head = /obj/item/clothing/head/helmet/leather/volfhelm + if(prob(50)) + neck = /obj/item/clothing/neck/coif + gloves = /obj/item/clothing/gloves/leather + H.base_strength = rand(12,14) //GENDER EQUALITY!! + H.base_speed = 11 + H.base_constitution = rand(10,12) //so their limbs no longer pop off like a skeleton + H.base_endurance = 13 + H.base_perception = 10 + H.base_intelligence = 10 + if(prob(50)) + r_hand = /obj/item/weapon/sword/short/iron + else + r_hand = /obj/item/weapon/mace/cudgel + if(prob(20)) + r_hand = /obj/item/weapon/sword/scimitar/falchion + if(prob(20)) + r_hand = /obj/item/weapon/pick + if(prob(25)) + l_hand = /obj/item/weapon/shield/wood + if(prob(10)) + l_hand = /obj/item/weapon/shield/tower/buckleriron + shoes = /obj/item/clothing/shoes/boots/leather + if(prob(30)) + neck = /obj/item/clothing/neck/leathercollar + H.set_eye_color("#27becc","#27becc") + H.set_hair_color("#61310f") + H.set_facial_hair_color(H.get_hair_color()) + if(H.gender == FEMALE) + H.set_hair_style(/datum/sprite_accessory/hair/head/messy) + else + H.set_hair_style(/datum/sprite_accessory/hair/head/messy) + H.set_facial_hair_style(/datum/sprite_accessory/hair/facial/manly) + H.adjust_skillrank(/datum/skill/combat/polearms, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/axesmaces, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/swords, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/shields, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/unarmed, 2, TRUE) // Trash mobs, untrained. + H.adjust_skillrank(/datum/skill/combat/wrestling, 2, TRUE) + H.adjust_skillrank(/datum/skill/misc/swimming, 2, TRUE) + H.adjust_skillrank(/datum/skill/misc/climbing, 2, TRUE) diff --git a/code/modules/mob/living/carbon/human/npc/human_soldiers/northern_milita.dm b/code/modules/mob/living/carbon/human/npc/human_soldiers/northern_milita.dm new file mode 100644 index 00000000000..768423fd9e3 --- /dev/null +++ b/code/modules/mob/living/carbon/human/npc/human_soldiers/northern_milita.dm @@ -0,0 +1,127 @@ +/mob/living/carbon/human/species/human/northern/militia //weak peasant infantry. Neutral but can be given factions for events. doesn't attack players. + ai_controller = /datum/ai_controller/human_npc + faction = list("neutral") + ambushable = FALSE + wander = TRUE + dodgetime = 30 + possible_rmb_intents = list() + var/is_silent = TRUE /// Determines whether or not we will scream our funny lines at people. + +/mob/living/carbon/human/species/human/northern/militia/Initialize() + . = ..() + AddComponent(/datum/component/ai_aggro_system) + set_species(/datum/species/human/northern) + addtimer(CALLBACK(src, PROC_REF(after_creation)), 1 SECONDS) + is_silent = TRUE + + +/mob/living/carbon/human/species/human/northern/militia/after_creation() + ..() + job = "Militia" + ADD_TRAIT(src, TRAIT_NOMOOD, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_NOHUNGER, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_LEECHIMMUNE, INNATE_TRAIT) + ADD_TRAIT(src, TRAIT_BREADY, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_MEDIUMARMOR, TRAIT_GENERIC) + equipOutfit(new /datum/outfit/job/human/species/human/northern/militia) + var/obj/item/organ/eyes/organ_eyes = getorgan(/obj/item/organ/eyes) + if(organ_eyes) + organ_eyes.eye_color = pick("27becc", "35cc27", "000000") + update_body() + +/datum/outfit/job/human/species/human/northern/militia/pre_equip(mob/living/carbon/human/H) + if(H.faction && ("viking" in H.faction)) + cloak = /obj/item/clothing/cloak/stabard/mercenary + else + cloak = /obj/item/clothing/cloak/stabard/guard + wrists = /obj/item/clothing/wrists/bracers/leather + shirt = /obj/item/clothing/armor/gambeson/light + if(prob(50)) + shirt = /obj/item/clothing/armor/gambeson + if(prob(25)) + armor = /obj/item/clothing/armor/leather + pants = /obj/item/clothing/pants/trou/leather + if(prob(50)) + pants = /obj/item/clothing/pants/trou + // Helmet, or lackthereof + switch(rand(1, 7)) + if(1) + head = /obj/item/clothing/head/helmet/kettle/iron + if(2) + head = /obj/item/clothing/head/helmet/sallet/iron + if(3) + head = /obj/item/clothing/head/helmet/skullcap + if(4 to 6) + head = /obj/item/clothing/neck/coif + if(7) + head = null + // Neck protection, if there's no coif on head + if(prob(50)) + neck = /obj/item/clothing/neck/coif + gloves = /obj/item/clothing/gloves/fingerless + if(prob(25)) + gloves = /obj/item/clothing/gloves/angle + H.base_strength = rand(10,11) //GENDER EQUALITY!! + H.base_speed = 10 + H.base_constitution = rand(10,12) //so their limbs no longer pop off like a skeleton + H.base_endurance = 10 + H.base_perception = 10 + H.base_intelligence = 10 + switch(rand(1, 11)) + // Militia Weapon. Of course they spawn with it + if(1) + r_hand = /obj/item/weapon/polearm/woodstaff + if(2) + r_hand = /obj/item/weapon/greataxe + if(3) + r_hand = /obj/item/weapon/polearm/spear + if(4) + r_hand = /obj/item/weapon/polearm/spear + l_hand = /obj/item/weapon/shield/wood + if(5) + r_hand = /obj/item/weapon/sickle/scythe + if(6) + r_hand = /obj/item/weapon/pick + if(7) + r_hand = /obj/item/weapon/sword/scimitar/falchion + if(8) + r_hand = /obj/item/weapon/mace/cudgel + if(9) + r_hand = /obj/item/weapon/mace/goden + if(10) + r_hand = /obj/item/weapon/axe/iron + l_hand = /obj/item/weapon/shield/wood + if(11) + r_hand = /obj/item/weapon/flail/peasantwarflail + shoes = /obj/item/clothing/shoes/boots/leather + var/eye_color = pick("27becc", "35cc27", "000000") + H.set_eye_color(eye_color,eye_color) + H.set_hair_color(pick("4f4f4f", "61310f", "faf6b9")) + H.set_facial_hair_color(H.get_hair_color()) + if(H.gender == FEMALE) + H.set_hair_style(pick(/datum/sprite_accessory/hair/head/countryponytailalt, /datum/sprite_accessory/hair/head/barbarian, /datum/sprite_accessory/hair/head/messy)) + else + H.set_hair_style(pick(/datum/sprite_accessory/hair/head/majestic_human, /datum/sprite_accessory/hair/head/messy, /datum/sprite_accessory/hair/head/barbarian)) + H.set_facial_hair_style(pick(/datum/sprite_accessory/hair/facial/viking, /datum/sprite_accessory/hair/facial/pick, /datum/sprite_accessory/hair/facial/manly)) + H.adjust_skillrank(/datum/skill/combat/polearms, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/axesmaces, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/swords, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/shields, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/unarmed, 2, TRUE) // Trash mobs, untrained. + H.adjust_skillrank(/datum/skill/combat/wrestling, 2, TRUE) + H.adjust_skillrank(/datum/skill/misc/swimming, 2, TRUE) + H.adjust_skillrank(/datum/skill/misc/climbing, 2, TRUE) + +/mob/living/carbon/human/species/human/northern/militia/ambush + wander = TRUE + +/mob/living/carbon/human/species/human/northern/militia/guard //variant that doesn't wander, if you want to place them as set dressing. will aggro enemies and animals + wander = FALSE + +/mob/living/carbon/human/species/human/northern/militia/deserter // Bad deserter, trash mob + faction = list("viking", "station") + +/mob/living/carbon/human/species/human/northern/militia/after_creation() + ..() + var/obj/item/bodypart/head/head = get_bodypart(BODY_ZONE_HEAD) + head.sellprice = 20 // Gobbo sellprice diff --git a/code/modules/mob/living/carbon/human/npc/human_soldiers/searaider.dm b/code/modules/mob/living/carbon/human/npc/human_soldiers/searaider.dm new file mode 100644 index 00000000000..2e3b714714b --- /dev/null +++ b/code/modules/mob/living/carbon/human/npc/human_soldiers/searaider.dm @@ -0,0 +1,127 @@ +GLOBAL_LIST_INIT(searaider_aggro, file2list("strings/rt/searaideraggrolines.txt")) + +/mob/living/carbon/human/species/human/northern/searaider + ai_controller = /datum/ai_controller/human_npc + faction = list("viking", "station") + ambushable = FALSE + dodgetime = 30 + flee_in_pain = TRUE + possible_rmb_intents = list() + var/is_silent = FALSE /// Determines whether or not we will scream our funny lines at people. + + +/mob/living/carbon/human/species/human/northern/searaider/ambush + ambushable = TRUE + +/mob/living/carbon/human/species/human/northern/searaider/Initialize() + . = ..() + AddComponent(/datum/component/ai_aggro_system) + set_species(/datum/species/human/northern) + addtimer(CALLBACK(src, PROC_REF(after_creation)), 1 SECONDS) + is_silent = TRUE + + +/mob/living/carbon/human/species/human/northern/searaider/after_creation() + ..() + job = "Sea Raider" + ADD_TRAIT(src, TRAIT_NOMOOD, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_NOHUNGER, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_HEAVYARMOR, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_LEECHIMMUNE, INNATE_TRAIT) + ADD_TRAIT(src, TRAIT_BREADY, TRAIT_GENERIC) + equipOutfit(new /datum/outfit/job/human/species/human/northern/searaider) + gender = pick(MALE, FEMALE) + var/obj/item/organ/eyes/organ_eyes = getorgan(/obj/item/organ/eyes) + var/obj/item/bodypart/head/head = get_bodypart(BODY_ZONE_HEAD) + var/hairf = pick(list(/datum/sprite_accessory/hair/head/barbarian, + /datum/sprite_accessory/hair/head/countryponytailalt)) + var/hairm = pick(list(/datum/sprite_accessory/hair/head/ponytailwitcher, + /datum/sprite_accessory/hair/head/barbarian)) + var/beard = pick(list(/datum/sprite_accessory/hair/facial/viking, + /datum/sprite_accessory/hair/facial/manly, + /datum/sprite_accessory/hair/facial/pick)) + head.sellprice = 30 // 50% More than gobbo + + var/datum/bodypart_feature/hair/head/new_hair = new() + var/datum/bodypart_feature/hair/facial/new_facial = new() + + if(gender == FEMALE) + new_hair.set_accessory_type(hairf, null, src) + else + new_hair.set_accessory_type(hairm, null, src) + new_facial.set_accessory_type(beard, null, src) + + if(prob(50)) + new_hair.accessory_colors = "#C1A287" + new_hair.hair_color = "#C1A287" + new_facial.accessory_colors = "#C1A287" + new_facial.hair_color = "#C1A287" + set_hair_color("#C1A287") + else + new_hair.accessory_colors = "#A56B3D" + new_hair.hair_color = "#A56B3D" + new_facial.accessory_colors = "#A56B3D" + new_facial.hair_color = "#A56B3D" + set_hair_color("#A56B3D") + + head.add_bodypart_feature(new_hair) + head.add_bodypart_feature(new_facial) + + dna.update_ui_block(DNA_HAIR_COLOR_BLOCK) + dna.species.handle_body(src) + + if(organ_eyes) + organ_eyes.eye_color = "#336699" + organ_eyes.accessory_colors = "#336699#336699" + + if(gender == FEMALE) + real_name = pick(file2list("strings/rt/names/human/vikingf.txt")) + else + real_name = pick(file2list("strings/rt/names/human/vikingm.txt")) + update_body() + + +/datum/outfit/job/human/species/human/northern/searaider/pre_equip(mob/living/carbon/human/H) + wrists = /obj/item/clothing/wrists/bracers/leather + if(prob(50)) + wrists = /obj/item/clothing/wrists/bracers/leather/advanced + armor = /obj/item/clothing/armor/chainmail/iron + shirt = /obj/item/clothing/shirt/undershirt/colored/vagrant + if(prob(50)) + shirt = /obj/item/clothing/shirt/tunic + pants = /obj/item/clothing/pants/tights + if(prob(50)) + pants = /obj/item/clothing/pants/chainlegs/iron + head = /obj/item/clothing/head/helmet/leather + if(prob(50)) + head = /obj/item/clothing/head/helmet/horned + if(prob(50)) + neck = /obj/item/clothing/neck/gorget + if(prob(50)) + gloves = /obj/item/clothing/gloves/leather + switch(rand(1, 4)) + if(1) + r_hand = /obj/item/weapon/sword/iron + l_hand = /obj/item/weapon/shield/wood + if(2) + r_hand = /obj/item/weapon/polearm/spear + if(3) + r_hand = /obj/item/weapon/greataxe + if(4) + r_hand = /obj/item/weapon/sword/long/greatsword + + shoes = /obj/item/clothing/shoes/boots/leather + H.base_speed = 9 + H.base_constitution = rand(10,12) //so their limbs no longer pop off like a skeleton + H.base_endurance = 15 + H.base_perception = 10 + H.base_intelligence = 1 + H.base_strength = 14 + H.adjust_skillrank(/datum/skill/combat/polearms, 3, TRUE) + H.adjust_skillrank(/datum/skill/combat/axesmaces, 3, TRUE) + H.adjust_skillrank(/datum/skill/combat/swords, 3, TRUE) + H.adjust_skillrank(/datum/skill/combat/shields, 3, TRUE) + H.adjust_skillrank(/datum/skill/combat/unarmed, 3, TRUE) + H.adjust_skillrank(/datum/skill/combat/wrestling, 3, TRUE) + H.adjust_skillrank(/datum/skill/misc/swimming, 2, TRUE) + H.adjust_skillrank(/datum/skill/misc/climbing, 2, TRUE) diff --git a/code/modules/mob/living/carbon/human/npc/human_soldiers/thieves.dm b/code/modules/mob/living/carbon/human/npc/human_soldiers/thieves.dm new file mode 100644 index 00000000000..e48ad98b7d2 --- /dev/null +++ b/code/modules/mob/living/carbon/human/npc/human_soldiers/thieves.dm @@ -0,0 +1,108 @@ +/mob/living/carbon/human/species/human/northern/thief //I'm a thief, give me your shit + faction = list("thieves") + ambushable = FALSE + dodgetime = 30 + flee_in_pain = TRUE + a_intent = INTENT_HELP + m_intent = MOVE_INTENT_SNEAK + d_intent = INTENT_DODGE + ai_controller = /datum/ai_controller/human_npc + +/mob/living/carbon/human/species/human/northern/thief/Initialize() + . = ..() + AddComponent(/datum/component/ai_aggro_system) + set_species(/datum/species/human/northern) + addtimer(CALLBACK(src, PROC_REF(after_creation)), 1 SECONDS) + +/mob/living/carbon/human/species/human/northern/thief/after_creation() + ..() + job = "Thief" + ADD_TRAIT(src, TRAIT_NOMOOD, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_NOHUNGER, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_LIGHT_STEP, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_DODGEEXPERT, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_LEECHIMMUNE, INNATE_TRAIT) + ADD_TRAIT(src, TRAIT_BREADY, TRAIT_GENERIC) + equipOutfit(new /datum/outfit/job/human/species/human/northern/thief) + gender = pick(MALE, FEMALE) + regenerate_icons() + + var/obj/item/organ/eyes/organ_eyes = getorgan(/obj/item/organ/eyes) + var/obj/item/bodypart/head/head = get_bodypart(BODY_ZONE_HEAD) + var/hairf = pick(list( + /datum/sprite_accessory/hair/head/bob)) + var/hairm = pick(list( + /datum/sprite_accessory/hair/head/shaved)) + var/beard = pick(list(/datum/sprite_accessory/hair/facial/vandyke)) + + var/datum/bodypart_feature/hair/head/new_hair = new() + var/datum/bodypart_feature/hair/facial/new_facial = new() + + if(gender == FEMALE) + new_hair.set_accessory_type(hairf, null, src) + else + new_hair.set_accessory_type(hairm, null, src) + new_facial.set_accessory_type(beard, null, src) + + if(prob(50)) + new_hair.accessory_colors = "#96403d" + new_hair.hair_color = "#96403d" + new_facial.accessory_colors = "#96403d" + new_facial.hair_color = "#96403d" + set_hair_color("#96403d") + else + new_hair.accessory_colors = "#C7C755" + new_hair.hair_color = "#C7C755" + new_facial.accessory_colors = "#C7C755" + new_facial.hair_color = "#C7C755" + set_hair_color("#C7C755") + + head.add_bodypart_feature(new_hair) + head.add_bodypart_feature(new_facial) + + dna.update_ui_block(DNA_HAIR_COLOR_BLOCK) + dna.species.handle_body(src) + + if(organ_eyes) + organ_eyes.eye_color = "#336699" + organ_eyes.accessory_colors = "#336699#336699" + + if(gender == FEMALE) + real_name = pick(file2list("strings/names/first_female.txt")) + else + real_name = pick(file2list("strings/names/first_male.txt")) + update_body() + head.sellprice = 30 + +/datum/outfit/job/human/species/human/northern/thief/pre_equip(mob/living/carbon/human/H) + cloak = /obj/item/clothing/cloak/raincloak/colored/mortus + wrists = /obj/item/clothing/wrists/bracers/leather + if(prob(50)) + wrists = /obj/item/clothing/wrists/bracers/copper + armor = /obj/item/clothing/armor/cuirass/copperchest + if(prob(50)) + armor = /obj/item/clothing/armor/leather + shirt = /obj/item/clothing/armor/gambeson/light + pants = /obj/item/clothing/pants/trou/leather + head = /obj/item/clothing/head/helmet/leather + mask = /obj/item/clothing/face/skullmask + neck = /obj/item/clothing/neck/gorget/copper + if(prob(50)) + neck = /obj/item/clothing/neck/leathercollar + gloves = /obj/item/clothing/gloves/leather + shoes = /obj/item/clothing/shoes/boots/leather + l_hand = /obj/item/weapon/knife/dagger + if(prob(50)) + l_hand = /obj/item/weapon/knife/copper + H.base_strength = 11 + H.base_speed = 16 + H.base_constitution = 11 + H.base_endurance = 11 + H.base_perception = 11 + H.base_intelligence = 1 + H.adjust_skillrank(/datum/skill/combat/knives, 3, TRUE) + H.adjust_skillrank(/datum/skill/combat/axesmaces, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/unarmed, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/wrestling, 2, TRUE) + H.adjust_skillrank(/datum/skill/misc/swimming, 2, TRUE) + H.adjust_skillrank(/datum/skill/misc/climbing, 2, TRUE) diff --git a/code/modules/mob/living/carbon/human/npc/human_soldiers/treasure_hunters.dm b/code/modules/mob/living/carbon/human/npc/human_soldiers/treasure_hunters.dm new file mode 100644 index 00000000000..dcf446be8cf --- /dev/null +++ b/code/modules/mob/living/carbon/human/npc/human_soldiers/treasure_hunters.dm @@ -0,0 +1,113 @@ +/* +* based on pages from elden ring in terms of visual design, these guys are intended to be a speedbump to solo adventurers at mount decap +* deadly but small in numbers. come back with a party, chump +*/ + +/mob/living/carbon/human/species/human/northern/mad_touched_treasure_hunter + ai_controller = /datum/ai_controller/human_npc + faction = list("viking", "station") + ambushable = FALSE + dodgetime = 15 + flee_in_pain = FALSE + possible_rmb_intents = list() + +/mob/living/carbon/human/species/human/northern/mad_touched_treasure_hunter/ambush + wander = TRUE + +/mob/living/carbon/human/species/human/northern/mad_touched_treasure_hunter/Initialize() + . = ..() + AddComponent(/datum/component/ai_aggro_system) + set_species(/datum/species/human/northern) + addtimer(CALLBACK(src, PROC_REF(after_creation)), 1 SECONDS) + +/mob/living/carbon/human/species/human/northern/mad_touched_treasure_hunter/after_creation() + ..() + job = "Mad-touched Treasure Hunter" + ADD_TRAIT(src, TRAIT_NOMOOD, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_NOHUNGER, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_MEDIUMARMOR, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_HEAVYARMOR, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_LEECHIMMUNE, INNATE_TRAIT) + ADD_TRAIT(src, TRAIT_DISFIGURED, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_CRITICAL_RESISTANCE, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_NOPAINSTUN, TRAIT_GENERIC) + equipOutfit(new /datum/outfit/job/human/species/human/northern/mad_touched_treasure_hunter) + var/obj/item/organ/eyes/organ_eyes = getorgan(/obj/item/organ/eyes) + if(organ_eyes) + organ_eyes.eye_color = pick("27becc", "35cc27", "000000") + update_body() + var/obj/item/bodypart/head/head = get_bodypart(BODY_ZONE_HEAD) + head.sellprice = 40 + +/datum/outfit/job/human/species/human/northern/mad_touched_treasure_hunter/pre_equip(mob/living/carbon/human/H) + wrists = /obj/item/clothing/wrists/bracers + mask = /obj/item/clothing/face/facemask/steel/mad_touched + armor = /obj/item/clothing/armor/leather/heavy + shirt = /obj/item/clothing/armor/gambeson + if(prob(20)) + shirt = /obj/item/clothing/armor/gambeson/light + pants = /obj/item/clothing/pants/platelegs + belt = /obj/item/storage/belt/leather + if(prob(33)) + beltl = /obj/item/reagent_containers/glass/bottle/healthpot + head = /obj/item/clothing/head/menacing/mad_touched_treasure_hunter + neck = /obj/item/clothing/neck/chaincoif + gloves = /obj/item/clothing/gloves/plate + cloak = /obj/item/clothing/cloak/wickercloak + if(prob(33)) + r_hand = /obj/item/weapon/sword/long/greatsword + else if(prob(33)) + r_hand = /obj/item/weapon/shield/tower/buckleriron + l_hand = /obj/item/weapon/knife/dagger/steel/dirk + else + r_hand = /obj/item/weapon/sword/sabre/hook + l_hand = /obj/item/weapon/sword/sabre/hook + + shoes = /obj/item/clothing/shoes/boots/leather + //carbon ai is still pretty dumb so making them a threat to players requires pretty crazy looking stats. don't think too hard about it. + H.base_strength = 15 + H.base_speed = 15 + H.base_constitution = 15 + H.base_endurance = 15 + H.base_perception = 15 + H.base_intelligence = 12 + H.set_eye_color("#27becc","#27becc") + H.set_hair_color("#61310f") + H.set_facial_hair_color(H.get_hair_color()) + if(H.gender == FEMALE) + H.set_hair_style(/datum/sprite_accessory/hair/head/messy) + else + H.set_hair_style(/datum/sprite_accessory/hair/head/messy) + H.set_facial_hair_style(/datum/sprite_accessory/hair/facial/manly) + + H.adjust_skillrank(/datum/skill/combat/polearms, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/axesmaces, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/swords, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/knives, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/shields, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/unarmed, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/wrestling, 4, TRUE) + H.adjust_skillrank(/datum/skill/misc/swimming, 2, TRUE) + H.adjust_skillrank(/datum/skill/misc/climbing, 2, TRUE) + H.real_name = pick(file2list("strings/rt/names/human/mad_touched_names.txt")) + +/obj/item/clothing/head/menacing/mad_touched_treasure_hunter //its here so it doesnt wind up on some class' loadout. + name = "sack hood" + desc = "A ragged hood of thick jute fibres. The itchiness is unbearable." + sewrepair = TRUE + color = "#999999" + armor = ARMOR_LEATHER + +/obj/item/clothing/face/facemask/steel/mad_touched + name = "eerie ancient mask" + +/obj/item/clothing/face/facemask/steel/mad_touched/equipped(mob/user, slot) + . = ..() + if(slot == ITEM_SLOT_MASK) + ADD_TRAIT(src, TRAIT_NODROP, CURSED_ITEM_TRAIT) + var/mob/living/carbon/human/mad_touched = user + mad_touched.apply_damage(25, BRUTE, BODY_ZONE_HEAD) + +/obj/item/clothing/face/facemask/steel/mad_touched/dropped(mob/user) + . = ..() + REMOVE_TRAIT(src, TRAIT_NODROP, CURSED_ITEM_TRAIT) diff --git a/code/modules/mob/living/carbon/human/npc/orc.dm b/code/modules/mob/living/carbon/human/npc/orc/_orc.dm similarity index 98% rename from code/modules/mob/living/carbon/human/npc/orc.dm rename to code/modules/mob/living/carbon/human/npc/orc/_orc.dm index 678d7bb6740..459af4db353 100644 --- a/code/modules/mob/living/carbon/human/npc/orc.dm +++ b/code/modules/mob/living/carbon/human/npc/orc/_orc.dm @@ -38,6 +38,7 @@ canparry = TRUE flee_in_pain = FALSE + var/orc_outfit wander = FALSE /mob/living/carbon/human/species/orc/npc/Initialize() @@ -45,6 +46,11 @@ AddComponent(/datum/component/ai_aggro_system) AddComponent(/datum/component/combat_noise, list("aggro" = 2)) +/mob/living/carbon/human/species/orc/npc/after_creation() + ..() + if(orc_outfit) + equipOutfit(new orc_outfit) + /mob/living/carbon/human/species/orc/ambush ai_controller = /datum/ai_controller/human_npc @@ -183,7 +189,7 @@ id = SPEC_ID_ORC species_traits = list(NO_UNDERWEAR) inherent_traits = list(TRAIT_RESISTCOLD,TRAIT_RESISTHIGHPRESSURE,TRAIT_RESISTLOWPRESSURE,TRAIT_RADIMMUNE,TRAIT_CRITICAL_WEAKNESS, TRAIT_NASTY_EATER, TRAIT_LEECHIMMUNE, TRAIT_INHUMENCAMP) - no_equip = list(ITEM_SLOT_SHIRT, ITEM_SLOT_MASK, ITEM_SLOT_GLOVES, ITEM_SLOT_SHOES, ITEM_SLOT_PANTS) + //no_equip = list(ITEM_SLOT_SHIRT, ITEM_SLOT_MASK, ITEM_SLOT_GLOVES, ITEM_SLOT_SHOES, ITEM_SLOT_PANTS) nojumpsuit = 1 sexes = 1 damage_overlay_type = "" diff --git a/code/modules/mob/living/carbon/human/npc/orc/soldiers.dm b/code/modules/mob/living/carbon/human/npc/orc/soldiers.dm new file mode 100644 index 00000000000..7766bac8263 --- /dev/null +++ b/code/modules/mob/living/carbon/human/npc/orc/soldiers.dm @@ -0,0 +1,180 @@ +/mob/living/carbon/human/species/orc/npc/footsoldier + orc_outfit = /datum/outfit/job/orc/npc/footsoldier + +/mob/living/carbon/human/species/orc/npc/marauder + orc_outfit = /datum/outfit/job/orc/npc/marauder + +/mob/living/carbon/human/species/orc/npc/berserker + orc_outfit = /datum/outfit/job/orc/npc/berserker + +/mob/living/carbon/human/species/orc/npc/warlord + orc_outfit = /datum/outfit/job/orc/npc/warlord + +/mob/living/carbon/human/species/orc/npc/archer_test + orc_outfit = /datum/outfit/job/orc/npc/archer_test + +// Underarmored orc with incomplete protection, bone axe / spear, and slow speed +/datum/outfit/job/orc/npc/footsoldier/pre_equip(mob/living/carbon/human/H) + name = "Orc Footsoldier" + wrists = /obj/item/clothing/wrists/bracers/leather + if(prob(50)) + armor = /obj/item/clothing/armor/leather/hide + else + armor = /obj/item/clothing/armor/leather + pants = /obj/item/clothing/pants/loincloth + if(prob(50)) + head = /obj/item/clothing/head/helmet/leather + shoes = /obj/item/clothing/shoes/gladiator + var/wepchoice = rand(1, 3) + switch(wepchoice) + if(1) + l_hand = /obj/item/weapon/axe/boneaxe + if(2) + l_hand = /obj/item/weapon/polearm/spear/bonespear + r_hand = /obj/item/weapon/shield/wood // Help preserve integrity + if(3) + l_hand = /obj/item/weapon/mace/cudgel + H.base_strength = 11 + H.base_speed = 8 + H.base_constitution = 11 + H.base_endurance = 11 + H.base_intelligence = 4 // Very dumb + H.adjust_skillrank(/datum/skill/combat/polearms, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/axesmaces, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/wrestling, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/unarmed, 2, TRUE) + H.adjust_skillrank(/datum/skill/misc/athletics, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/shields, 2, TRUE) + H.adjust_skillrank(/datum/skill/misc/climbing, 2, TRUE) + H.adjust_skillrank(/datum/skill/misc/swimming, 2, TRUE) + +// Slightly armored orc with slight facial protection, incomplete chainmail and spear / sword +/datum/outfit/job/orc/npc/marauder/pre_equip(mob/living/carbon/human/H) + name = "Orc Marauder" + wrists = /obj/item/clothing/wrists/bracers/leather + armor = /obj/item/clothing/armor/chainmail + shirt = /obj/item/clothing/armor/gambeson/light + pants = /obj/item/clothing/pants/chainlegs/iron + neck = /obj/item/clothing/neck/coif + head = /obj/item/clothing/head/helmet/leather + mask = /obj/item/clothing/face/facemask + shoes = /obj/item/clothing/shoes/boots/leather/advanced + var/wepchoice = rand(1, 5) + switch(wepchoice) + if(1) + l_hand = /obj/item/weapon/polearm/spear + if(2) + l_hand = /obj/item/weapon/sword/scimitar/falchion + r_hand = /obj/item/weapon/shield/wood // Help preserve integrity + if(3) + l_hand = /obj/item/weapon/mace // Threat to parry-er + if(4) + l_hand = /obj/item/weapon/greataxe + if(5) + l_hand = /obj/item/weapon/pick + H.base_strength = 12 // GAGGER GAGGER GAGGER + H.base_speed = 8 + H.base_constitution = 12 + H.base_endurance = 10 + H.base_intelligence = 4 + H.adjust_skillrank(/datum/skill/combat/polearms, 3, TRUE) + H.adjust_skillrank(/datum/skill/combat/swords, 3, TRUE) + H.adjust_skillrank(/datum/skill/combat/axesmaces, 3, TRUE) + H.adjust_skillrank(/datum/skill/combat/wrestling, 3, TRUE) + H.adjust_skillrank(/datum/skill/labor/mining, 3, TRUE) + H.adjust_skillrank(/datum/skill/combat/unarmed, 3, TRUE) + H.adjust_skillrank(/datum/skill/misc/athletics, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/shields, 2, TRUE) + H.adjust_skillrank(/datum/skill/misc/climbing, 2, TRUE) + H.adjust_skillrank(/datum/skill/misc/swimming, 2, TRUE) + ADD_TRAIT(H, TRAIT_MEDIUMARMOR, TRAIT_GENERIC) + +// Lightly armored orc in light armor with no pain stun, and grappling oriented weapons +/datum/outfit/job/orc/npc/berserker/pre_equip(mob/living/carbon/human/H) + wrists = /obj/item/clothing/wrists/bracers/leather + armor = /obj/item/clothing/armor/leather/hide + shirt = /obj/item/clothing/armor/gambeson/light + pants = /obj/item/clothing/pants/trou/leather + head = /obj/item/clothing/head/helmet/leather + neck = /obj/item/clothing/neck/coif + mask = /obj/item/clothing/face/facemask + shoes = /obj/item/clothing/shoes/boots/leather/advanced + var/wepchoice = rand(1, 2) + switch(wepchoice) + if(1) + l_hand = /obj/item/weapon/knife/dagger + if(2) + l_hand = /obj/item/weapon/pick + H.base_strength = 13 // GAGGER GAGGER GAGGER + H.base_speed = 10 // Fast, for an orc + H.base_constitution = 12 + H.base_endurance = 12 + H.base_intelligence = 1 // Minmax department + H.adjust_skillrank(/datum/skill/combat/knives, 3, TRUE) + H.adjust_skillrank(/datum/skill/combat/wrestling, 3, TRUE) + H.adjust_skillrank(/datum/skill/labor/mining, 3, TRUE) + H.adjust_skillrank(/datum/skill/combat/unarmed, 3, TRUE) + H.adjust_skillrank(/datum/skill/misc/athletics, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/shields, 2, TRUE) + H.adjust_skillrank(/datum/skill/misc/climbing, 2, TRUE) + H.adjust_skillrank(/datum/skill/misc/swimming, 2, TRUE) + ADD_TRAIT(H, TRAIT_NOPAINSTUN, TRAIT_GENERIC) + ADD_TRAIT(H, TRAIT_CRITICAL_RESISTANCE, INNATE_TRAIT) + +// Heavily armored orc with complete iron protection, heavy armor, and a two hander. +/datum/outfit/job/orc/npc/warlord/pre_equip(mob/living/carbon/human/H) + wrists = /obj/item/clothing/wrists/bracers/leather/advanced + armor = /obj/item/clothing/armor/chainmail + shirt = /obj/item/clothing/armor/gambeson + pants = /obj/item/clothing/pants/chainlegs/iron + head = /obj/item/clothing/head/helmet/skullcap + neck = /obj/item/clothing/neck/chaincoif/iron + mask = /obj/item/clothing/face/facemask + shoes = /obj/item/clothing/shoes/boots/leather/advanced + var/wepchoice = rand(1, 6) + switch(wepchoice) + if(1) + l_hand = /obj/item/weapon/polearm/halberd/bardiche + if(2) + l_hand = /obj/item/weapon/polearm/halberd + if(3) + l_hand = /obj/item/weapon/greataxe + if(4) + l_hand = /obj/item/weapon/polearm/eaglebeak/lucerne + if(5) + l_hand = /obj/item/weapon/mace/goden + if(6) + l_hand = /obj/item/weapon/sword/scimitar/falchion + r_hand = /obj/item/weapon/sword/scimitar/falchion // intrusive thoughts + H.base_strength = 14 // GAGGER GAGGER GAGGER + H.base_speed = 10 // Fast, for an orc + H.base_constitution = 12 + H.base_endurance = 12 + H.base_intelligence = 1 + H.adjust_skillrank(/datum/skill/combat/polearms, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/swords, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/axesmaces, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/wrestling, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/unarmed, 4, TRUE) + H.adjust_skillrank(/datum/skill/misc/athletics, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/shields, 3, TRUE) + H.adjust_skillrank(/datum/skill/misc/climbing, 2, TRUE) + H.adjust_skillrank(/datum/skill/misc/swimming, 2, TRUE) + ADD_TRAIT(H, TRAIT_HEAVYARMOR, TRAIT_GENERIC) + + +// Heavily armored orc with complete iron protection, heavy armor, and a two hander. +/datum/outfit/job/orc/npc/archer_test/pre_equip(mob/living/carbon/human/H) + backr = /obj/item/gun/ballistic/revolver/grenadelauncher/bow + backl = /obj/item/ammo_holder/quiver/arrows + l_hand = /obj/item/weapon/sword/short/iron + armor = /obj/item/clothing/armor/leather/hide + shirt = /obj/item/clothing/armor/gambeson/light + pants = /obj/item/clothing/pants/trou/leather + belt = /obj/item/storage/belt/leather/knifebelt/black/steel + beltr = /obj/item/storage/belt/pouch/medicine + H.base_strength = 14 // GAGGER GAGGER GAGGER + H.base_speed = 10 // Fast, for an orc + H.base_constitution = 12 + H.base_endurance = 12 + H.base_intelligence = 1 diff --git a/code/modules/mob/living/carbon/human/npc/skeleton.dm b/code/modules/mob/living/carbon/human/npc/skeleton/_skeleton.dm similarity index 100% rename from code/modules/mob/living/carbon/human/npc/skeleton.dm rename to code/modules/mob/living/carbon/human/npc/skeleton/_skeleton.dm diff --git a/code/modules/mob/living/carbon/human/npc/skeleton/difficulties.dm b/code/modules/mob/living/carbon/human/npc/skeleton/difficulties.dm new file mode 100644 index 00000000000..3fdc5ace5be --- /dev/null +++ b/code/modules/mob/living/carbon/human/npc/skeleton/difficulties.dm @@ -0,0 +1,233 @@ +// Ultra easy tier skeleton with no armor and just a single weapon. +/mob/living/carbon/human/species/skeleton/npc/supereasy + skel_outfit = /datum/outfit/job/skeleton/npc/supereasy + + +/datum/outfit/job/skeleton/npc/supereasy/pre_equip(mob/living/carbon/human/H) + ..() + H.base_strength = 10 + H.base_speed = 8 + H.base_constitution = 4 + H.base_endurance = 10 + H.base_intelligence = 1 + name = "Skeleton" + if(prob(50)) + shirt = /obj/item/clothing/shirt/rags + else + shirt = /obj/item/clothing/shirt/tunic/colored/random + if(prob(50)) + pants = /obj/item/clothing/pants/tights/colored/random + else + pants = /obj/item/clothing/pants/loincloth + var/weapon_choice = rand(1, 4) + switch(weapon_choice) + if(1) + r_hand = /obj/item/weapon/axe/iron + if(2) + r_hand = /obj/item/weapon/sword/short/iron + if(3) + r_hand = /obj/item/weapon/polearm/spear/bonespear + if(4) + r_hand = /obj/item/weapon/mace + + H.adjust_skillrank(/datum/skill/combat/polearms, 1, TRUE) + H.adjust_skillrank(/datum/skill/combat/axesmaces, 1, TRUE) + H.adjust_skillrank(/datum/skill/combat/swords, 1, TRUE) + H.adjust_skillrank(/datum/skill/combat/knives, 1, TRUE) + H.adjust_skillrank(/datum/skill/combat/shields, 1, TRUE) + H.adjust_skillrank(/datum/skill/combat/unarmed, 1, TRUE) + H.adjust_skillrank(/datum/skill/combat/wrestling, 1, TRUE) + H.adjust_skillrank(/datum/skill/misc/swimming, 2, TRUE) + H.adjust_skillrank(/datum/skill/misc/climbing, 2, TRUE) + +// Easy tier skeleton, with only incomplete chainmail and kilt +// Ambushes people in "safe" route. A replacement for old skeletons that were effectively naked. +/mob/living/carbon/human/species/skeleton/npc/easy + skel_outfit = /datum/outfit/job/skeleton/npc/easy + + +/datum/outfit/job/skeleton/npc/easy/pre_equip(mob/living/carbon/human/H) + ..() + H.base_strength = 9 + H.base_speed = 8 + H.base_constitution = 4 // Same statblock as before easily killed + H.base_endurance = 12 + H.base_intelligence = 1 + name = "Skeleton Footsoldier" + shirt = /obj/item/clothing/armor/chainmail + pants = /obj/item/clothing/pants/chainlegs/kilt + shoes = /obj/item/clothing/shoes/boots/armor/light + var/weapon_choice = rand(1, 4) + switch(weapon_choice) + if(1) + r_hand = /obj/item/weapon/axe/iron + if(2) + r_hand = /obj/item/weapon/sword/short/iron + if(3) + r_hand = /obj/item/weapon/polearm/spear/bonespear + if(4) + r_hand = /obj/item/weapon/mace + + H.adjust_skillrank(/datum/skill/combat/polearms, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/axesmaces, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/swords, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/knives, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/shields, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/unarmed, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/wrestling, 2, TRUE) + H.adjust_skillrank(/datum/skill/misc/swimming, 2, TRUE) + H.adjust_skillrank(/datum/skill/misc/climbing, 2, TRUE) + +// Also an "easy" tier skeleton, pirate themed, with a free hand to grab you +/mob/living/carbon/human/species/skeleton/npc/pirate + skel_outfit = /datum/outfit/job/skeleton/npc/pirate + +/datum/outfit/job/skeleton/npc/pirate/pre_equip(mob/living/carbon/human/H) + ..() + H.base_strength = 9 + H.base_speed = 8 + H.base_constitution = 4 // Same statblock as before easily killed + H.base_endurance = 12 + H.base_intelligence = 1 + name = "Skeleton Pirate" + head = /obj/item/clothing/head/helmet/leather/tricorn + wrists = /obj/item/clothing/wrists/bracers/ancient + shirt = /obj/item/clothing/armor/chainmail/iron + pants = /obj/item/clothing/pants/tights/sailor + shoes = /obj/item/clothing/shoes/boots/armor/light + if(prob(50)) + r_hand = /obj/item/weapon/knife/dagger + else + r_hand = /obj/item/weapon/knuckles + + H.adjust_skillrank(/datum/skill/combat/polearms, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/axesmaces, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/swords, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/knives, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/shields, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/unarmed, 2, TRUE) + H.adjust_skillrank(/datum/skill/combat/wrestling, 2, TRUE) + H.adjust_skillrank(/datum/skill/misc/swimming, 2, TRUE) + H.adjust_skillrank(/datum/skill/misc/climbing, 2, TRUE) + +// Medium tier skeleton, 3 skills. +/mob/living/carbon/human/species/skeleton/npc/medium + skel_outfit = /datum/outfit/job/skeleton/npc/medium + + +/datum/outfit/job/skeleton/npc/medium/pre_equip(mob/living/carbon/human/H) + ..() + H.base_strength = 11 + H.base_speed = 8 + H.base_constitution = 6 // Slightly tougher now! + H.base_endurance = 10 + H.base_intelligence = 1 + name = "Skeleton Soldier" + cloak = /obj/item/clothing/cloak/heartfelt // Ooo Spooky Old Dead MAA + head = /obj/item/clothing/head/helmet/heavy/ancient + armor = /obj/item/clothing/armor/cuirass/copperchest + shirt = /obj/item/clothing/armor/chainmail/iron + wrists = /obj/item/clothing/wrists/bracers/ancient + pants = /obj/item/clothing/pants/chainlegs/kilt/iron + shoes = /obj/item/clothing/shoes/boots/armor/light + neck = /obj/item/clothing/neck/chaincoif/iron + gloves = /obj/item/clothing/gloves/chain + belt = /obj/item/storage/belt/leather/rope + if(prob(33)) // 33% chance of shield, so ranged don't get screwed over entirely + l_hand = /obj/item/weapon/shield/tower/metal/ancient + if(prob(33)) + r_hand = /obj/item/weapon/polearm/spear/bonespear + else if(prob(33)) + r_hand = /obj/item/weapon/sword/gladius + else + r_hand = /obj/item/weapon/flail + + H.adjust_skillrank(/datum/skill/combat/polearms, 3, TRUE) + H.adjust_skillrank(/datum/skill/combat/axesmaces, 3, TRUE) + H.adjust_skillrank(/datum/skill/combat/swords, 3, TRUE) + H.adjust_skillrank(/datum/skill/combat/knives, 3, TRUE) + H.adjust_skillrank(/datum/skill/combat/shields, 3, TRUE) + H.adjust_skillrank(/datum/skill/combat/unarmed, 3, TRUE) + H.adjust_skillrank(/datum/skill/combat/wrestling, 3, TRUE) + H.adjust_skillrank(/datum/skill/misc/swimming, 3, TRUE) + H.adjust_skillrank(/datum/skill/misc/climbing, 3, TRUE) + +// High tier skeleton, 4 skills. Heavy Armor. +/mob/living/carbon/human/species/skeleton/npc/hard + skel_outfit = /datum/outfit/job/skeleton/npc/hard + +/datum/outfit/job/skeleton/npc/hard/pre_equip(mob/living/carbon/human/H) + ..() + H.base_strength = 12 + H.base_constitution = 8 // Woe, actual limb health. + H.base_endurance = 12 + H.base_intelligence = 1 + name = "Skeleton Dreadnought" + // This combines the khopesh and withered dreadknight + var/skeletonclass = rand(1, 2) + if(skeletonclass == 1) // Khopesh Knight + H.base_speed = 12 // Hue + cloak = /obj/item/clothing/cloak/heartfelt + mask = /obj/item/clothing/face/facemask/copper + armor = /obj/item/clothing/armor/cuirass/copperchest + shirt = /obj/item/clothing/armor/chainmail/iron + wrists = /obj/item/clothing/wrists/bracers/ancient + pants = /obj/item/clothing/pants/platelegs/iron + shoes = /obj/item/clothing/shoes/boots/armor/light + neck = /obj/item/clothing/neck/psycross/zizo + gloves = /obj/item/clothing/gloves/chain/iron + r_hand = /obj/item/weapon/sword/sabre/cutlass + l_hand = /obj/item/weapon/sword/sabre/cutlass + else // Withered Dreadknight + H.base_speed = 8 + cloak = /obj/item/clothing/cloak/tabard/blkknight + head = /obj/item/clothing/head/helmet/heavy/ironplate + armor = /obj/item/clothing/armor/plate/ancient + shirt = /obj/item/clothing/armor/chainmail/hauberk/fluted + wrists = /obj/item/clothing/wrists/bracers/ancient + pants = /obj/item/clothing/pants/platelegs/ancient + shoes = /obj/item/clothing/shoes/boots/armor/light + neck = /obj/item/clothing/neck/gorget/ancient + gloves = /obj/item/clothing/gloves/plate/ancient + belt = /obj/item/storage/belt/leather + if(prob(50)) + r_hand = /obj/item/weapon/sword/long/greatsword + else + r_hand = /obj/item/weapon/mace/goden + + H.adjust_skillrank(/datum/skill/combat/polearms, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/axesmaces, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/swords, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/knives, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/shields, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/unarmed, 4, TRUE) + H.adjust_skillrank(/datum/skill/combat/wrestling, 4, TRUE) + H.adjust_skillrank(/datum/skill/misc/swimming, 4, TRUE) + H.adjust_skillrank(/datum/skill/misc/climbing, 4, TRUE) + +// For Duke Manor & Zizo Manor - Ground based spread, so no pirate in pool! +/mob/living/carbon/human/species/skeleton/npc/mediumspread/Initialize() + var/outfit = rand(1, 4) + switch(outfit) + if(1) + skel_outfit = /datum/outfit/job/skeleton/npc/supereasy + if(2) + skel_outfit = /datum/outfit/job/skeleton/npc/easy + if(3) + skel_outfit = /datum/outfit/job/skeleton/npc/medium + if(4) + skel_outfit = /datum/outfit/job/skeleton/npc/hard + . = ..() + +/mob/living/carbon/human/species/skeleton/npc/hardspread/Initialize() + var/outfit = rand(1,4) + switch(outfit) + if(1) + skel_outfit = /datum/outfit/job/skeleton/npc/hard + if(2) + skel_outfit = /datum/outfit/job/skeleton/npc/medium + if(3) + skel_outfit = /datum/outfit/job/skeleton/npc/pirate + if(4) + skel_outfit = /datum/outfit/job/skeleton/npc/hard + . = ..() diff --git a/code/modules/mob/living/carbon/human/species_types/automatons/_automaton.dm b/code/modules/mob/living/carbon/human/species_types/automatons/_automaton.dm index 8fd0e8b06bc..a21df04c887 100644 --- a/code/modules/mob/living/carbon/human/species_types/automatons/_automaton.dm +++ b/code/modules/mob/living/carbon/human/species_types/automatons/_automaton.dm @@ -11,6 +11,14 @@ . = ..() AddComponent(/datum/component/ghost_vessel) +/mob/living/carbon/human/species/automaton/vessel/LateInitialize() + . = ..() + AddComponent(/datum/component/ghost_vessel, /obj/item/reagent_containers/lux) + +/mob/living/carbon/human/species/automaton/prefilled_vessel/LateInitialize() + . = ..() + AddComponent(/datum/component/ghost_vessel) + /datum/species/automaton name = "Automaton" id = SPEC_ID_AUTOMATON diff --git a/code/modules/mob/living/carbon/monkey/combat.dm b/code/modules/mob/living/carbon/monkey/combat.dm index 3dc6848837d..8fe9f3f8a5b 100644 --- a/code/modules/mob/living/carbon/monkey/combat.dm +++ b/code/modules/mob/living/carbon/monkey/combat.dm @@ -103,7 +103,7 @@ monkeyDrop(get_item_by_slot(C)) // remove the existing item if worn addtimer(CALLBACK(src, PROC_REF(equip_to_appropriate_slot), C), 5) -/mob/living/carbon/monkey/resist_restraints() +/mob/living/carbon/monkey/resist_restraints(instant = FALSE) var/obj/item/I = null if(handcuffed) I = handcuffed diff --git a/code/modules/mob/living/carbon/spirit/combat.dm b/code/modules/mob/living/carbon/spirit/combat.dm index 4b63b0610b2..4cc1dc78b3a 100644 --- a/code/modules/mob/living/carbon/spirit/combat.dm +++ b/code/modules/mob/living/carbon/spirit/combat.dm @@ -96,7 +96,7 @@ monkeyDrop(get_item_by_slot(C)) // remove the existing item if worn addtimer(CALLBACK(src, PROC_REF(equip_to_appropriate_slot), C), 5) -/mob/living/carbon/spirit/resist_restraints() +/mob/living/carbon/spirit/resist_restraints(instant = FALSE) var/obj/item/I = null if(handcuffed) I = handcuffed diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index 70195ee2860..dbb353a467c 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -1661,7 +1661,7 @@ /mob/living/proc/resist_fire() return -/mob/living/proc/resist_restraints() +/mob/living/proc/resist_restraints(instant = FALSE) return /mob/living/proc/get_visible_name() diff --git a/code/modules/mob/living/simple_animal/examine.dm b/code/modules/mob/living/simple_animal/examine.dm index 4d180405fb7..b476159ddc0 100644 --- a/code/modules/mob/living/simple_animal/examine.dm +++ b/code/modules/mob/living/simple_animal/examine.dm @@ -108,5 +108,15 @@ if(desc) . += desc + if(ssaddle) + . += span_notice("This animal is saddled: ([ssaddle.name]).") + if(ccaparison) + . += span_notice("This animal is wearing a caparison: ([ccaparison.name]).") + if(bbarding) + . += span_notice("This animal is wearing a bard: ([bbarding.name]).") + + if(genetics && length(genetics.genes)) + . += span_notice("Genetic traits: [english_list(genetics.get_gene_names())].") + . += "ᛉ ------------ ᛉ" SEND_SIGNAL(src, COMSIG_PARENT_EXAMINE, user, .) diff --git a/code/modules/mob/living/simple_animal/friendly/cat.dm b/code/modules/mob/living/simple_animal/friendly/cat.dm index 21844051181..c511ea1a613 100644 --- a/code/modules/mob/living/simple_animal/friendly/cat.dm +++ b/code/modules/mob/living/simple_animal/friendly/cat.dm @@ -157,9 +157,6 @@ matrix.Scale(0.5, 0.5) transform = matrix -/mob/living/simple_animal/pet/cat/proc/after_birth(mob/living/simple_animal/pet/cat/kitten/baby, mob/living/partner) - return - /mob/living/simple_animal/pet/cat/proc/wuv(change, mob/M) if(change) if(change > 0) diff --git a/code/modules/mob/living/simple_animal/genetics/animal_genes/_animal_gene.dm b/code/modules/mob/living/simple_animal/genetics/animal_genes/_animal_gene.dm new file mode 100644 index 00000000000..d288105cb03 --- /dev/null +++ b/code/modules/mob/living/simple_animal/genetics/animal_genes/_animal_gene.dm @@ -0,0 +1,66 @@ +/datum/animal_gene + var/name = "Unknown Gene" + var/desc = "An unknown genetic trait." + var/rarity = 10 + var/exclusion_group = null + var/dominant = TRUE + var/recessive_state = RECESSIVE_NONE + ///if set this is the odds of emergence actually working (useful for non requirement emergence parts) + var/emergence_chance + var/intensity = 1.0 + var/intensity_min = 10 + var/intensity_max = 10 + var/applied = FALSE + var/gene_flags = NONE + var/list/type_whitelist = null + /// List of gene types, each parent must contribute at least one matching gene + /// for this gene to be eligible during breeding. Only checked when GENE_FLAG_EMERGENCE is set. + var/list/required_parent_genes = null + +/datum/animal_gene/New() + if(intensity_min != intensity_max) + intensity = rand(intensity_min, intensity_max) * 0.1 + else + intensity = intensity_min + +/datum/animal_gene/proc/allowed_for(mob/living/simple_animal/hostile/target) + if(!type_whitelist) + return TRUE + return is_type_in_list(target, type_whitelist) + +/datum/animal_gene/proc/apply_to(mob/living/simple_animal/hostile/target) + SHOULD_CALL_PARENT(TRUE) + var/old_apply = applied + applied = TRUE + return old_apply + +/datum/animal_gene/proc/remove_from(mob/living/simple_animal/hostile/target) + SHOULD_CALL_PARENT(TRUE) + var/old_apply = applied + applied = FALSE + return old_apply + +/// Produce an offspring gene from this gene and an optional matching gene from the other parent. +/// If other is null (unpaired), intensity is preserved with minor noise. +/// If paired, intensity trends toward the higher of the two, selective breeding accumulates. +/datum/animal_gene/proc/breed_with(datum/animal_gene/other) + var/datum/animal_gene/offspring = new type() + offspring.dominant = dominant + if(!offspring.dominant) + // Expression state is determined by _breed_pools based on whether both parents contributed + // Always start as CARRIED here; _breed_pools will upgrade to EXPRESSED if warranted + offspring.recessive_state = RECESSIVE_CARRIED + if(!other) + // Unpaired: pass with minor noise, floored at GENETICS_INTENSITY_FLOOR of intensity_min + var/noise = (rand(-GENETICS_INTENSITY_NOISE, GENETICS_INTENSITY_NOISE) * 0.1) * intensity + offspring.intensity = clamp(intensity + noise, intensity_min * GENETICS_INTENSITY_FLOOR, intensity_max) + return offspring + // Paired: average biased toward the higher value (stronger gene wins more often), + // plus noise. This means two fat parents consistently produce fatter offspring. + var/lo = min(intensity, other.intensity) + var/hi = max(intensity, other.intensity) + // Bias: pick in the upper 60% of the lo-hi range, then add noise + var/base = lo + (rand(4, 10) * 0.1) * (hi - lo) + var/noise = (rand(-GENETICS_INTENSITY_NOISE, GENETICS_INTENSITY_NOISE) * 0.1) * hi + offspring.intensity = clamp(base + noise, (intensity_min * GENETICS_INTENSITY_FLOOR) * 0.1, intensity_max * 0.1) + return offspring diff --git a/code/modules/mob/living/simple_animal/genetics/animal_genes/aggressive.dm b/code/modules/mob/living/simple_animal/genetics/animal_genes/aggressive.dm new file mode 100644 index 00000000000..4096becc862 --- /dev/null +++ b/code/modules/mob/living/simple_animal/genetics/animal_genes/aggressive.dm @@ -0,0 +1,36 @@ +/datum/animal_gene/aggressive + name = "Aggressive" + desc = "Territorial. Will attack nearby creatures unprovoked." + rarity = 3 + dominant = TRUE + exclusion_group = GENE_GROUP_TEMPERAMENT + + +/datum/animal_gene/aggressive/apply_to(mob/living/simple_animal/hostile/target) + if(..()) + return + if(!target.ai_controller) + return + target.ai_controller.remove_subtree(/datum/ai_planning_subtree/find_nearest_thing_which_attacked_me_to_flee) + target.ai_controller.remove_subtree(/datum/ai_planning_subtree/flee_target) + target.ai_controller.remove_subtree(/datum/ai_planning_subtree/look_for_adult) + target.ai_controller.add_subtree_at(/datum/ai_planning_subtree/aggro_find_target, 1) + var/aggro_index = target.ai_controller.get_subtree_index(/datum/ai_planning_subtree/aggro_find_target) + target.ai_controller.add_subtree_at(/datum/ai_planning_subtree/basic_melee_attack_subtree, aggro_index + 1) + if(!target.GetComponent(/datum/component/ai_aggro_system)) + target.AddComponent(/datum/component/ai_aggro_system) + target.melee_damage_lower = max(target.melee_damage_lower, 3) + target.melee_damage_upper = max(target.melee_damage_upper, 6) + +/datum/animal_gene/aggressive/remove_from(mob/living/simple_animal/hostile/target) + if(!..()) + return + if(!target.ai_controller) + return + target.ai_controller.remove_subtree(/datum/ai_planning_subtree/aggro_find_target) + target.ai_controller.remove_subtree(/datum/ai_planning_subtree/basic_melee_attack_subtree) + target.ai_controller.add_subtree_at(/datum/ai_planning_subtree/find_nearest_thing_which_attacked_me_to_flee) + target.ai_controller.add_subtree_at(/datum/ai_planning_subtree/flee_target) + qdel(target.GetComponent(/datum/component/ai_aggro_system)) + target.melee_damage_lower = initial(target.melee_damage_lower) + target.melee_damage_upper = initial(target.melee_damage_upper) diff --git a/code/modules/mob/living/simple_animal/genetics/animal_genes/barren.dm b/code/modules/mob/living/simple_animal/genetics/animal_genes/barren.dm new file mode 100644 index 00000000000..7a14a93e726 --- /dev/null +++ b/code/modules/mob/living/simple_animal/genetics/animal_genes/barren.dm @@ -0,0 +1,29 @@ +/datum/animal_gene/barren + name = "Barren" + desc = "Dulls reproductive drive, significantly extending the cooldown between breeding cycles." + rarity = 6 + exclusion_group = GENE_GROUP_BREEDING + intensity_min = 1 + intensity_max = 10 + +/datum/animal_gene/barren/apply_to(mob/living/simple_animal/target) + . = ..() + if(.) + return + var/datum/component/breed/breed_component = target.GetComponent(/datum/component/breed) + if(!breed_component) + return + // At max intensity, timer is doubled. At min, barely changed. + // Multiplier lands between ~1.05 (min) and 2.0 (max) + var/multiplier = 1.0 + (intensity * 1.0) + breed_component.breed_timer = breed_component.breed_timer * multiplier + +/datum/animal_gene/barren/remove_from(mob/living/simple_animal/target) + . = ..() + if(!.) + return + var/datum/component/breed/breed_component = target.GetComponent(/datum/component/breed) + if(!breed_component) + return + var/multiplier = 1.0 + (intensity * 1.0) + breed_component.breed_timer = breed_component.breed_timer / multiplier diff --git a/code/modules/mob/living/simple_animal/genetics/animal_genes/coat.dm b/code/modules/mob/living/simple_animal/genetics/animal_genes/coat.dm new file mode 100644 index 00000000000..2a77c4fc1f6 --- /dev/null +++ b/code/modules/mob/living/simple_animal/genetics/animal_genes/coat.dm @@ -0,0 +1,71 @@ +/datum/animal_gene/coat_color + name = "Coat Color" + exclusion_group = GENE_GROUP_COAT_COLOR + dominant = TRUE + rarity = 10 + abstract_type = /datum/animal_gene/coat_color + gene_flags = GENE_FLAG_INTRINSIC + type_whitelist = list( + /mob/living/simple_animal/hostile/retaliate/honse, + /mob/living/simple_animal/hostile/retaliate/saiga, + /mob/living/simple_animal/hostile/retaliate/saigabuck, + /mob/living/simple_animal/hostile/retaliate/saiga/saigakid, + /mob/living/simple_animal/hostile/retaliate/saiga/saigakid/boy, + ) + +/datum/animal_gene/coat_color/breed_with(datum/animal_gene/other) + if(!other) + return new type() + // 50/50 which parent's coat the foal inherits + if(prob(50)) + return new type() + return new other.type() + +/datum/animal_gene/coat_color/proc/get_color() + return COLOR_WHITE + +/datum/animal_gene/coat_color/apply_to(mob/living/simple_animal/target) + . = ..() + if(istype(target, /mob/living/simple_animal/hostile/retaliate/saiga) || istype(target, /mob/living/simple_animal/hostile/retaliate/saigabuck)) + return + target.color = get_color() + +/datum/animal_gene/coat_color/remove_from(mob/living/simple_animal/target) + . = ..() + target.color = null + +/datum/animal_gene/coat_color/white + name = "White Coat" +/datum/animal_gene/coat_color/white/get_color() + return COLOR_WHITE + +/datum/animal_gene/coat_color/gray + name = "Gray Coat" +/datum/animal_gene/coat_color/gray/get_color() + return COLOR_GRAY + +/datum/animal_gene/coat_color/black + name = "Black Coat" +/datum/animal_gene/coat_color/black/get_color() + return COLOR_ALMOST_BLACK + +/datum/animal_gene/coat_color/brown + name = "Brown Coat" +/datum/animal_gene/coat_color/brown/get_color() + return COLOR_DARK_BROWN + +/datum/animal_gene/coat_color/chestnut + name = "Chestnut Coat" +/datum/animal_gene/coat_color/chestnut/get_color() + return COLOR_DARK_ORANGE + +/datum/animal_gene/coat_color/silver_dapple + name = "Silver Dapple Coat" + gene_flags = GENE_FLAG_INTRINSIC | GENE_FLAG_EMERGENCE + required_parent_genes = list( + /datum/animal_gene/coat_color/black, + /datum/animal_gene/coat_color/chestnut + ) + +/datum/animal_gene/coat_color/silver_dapple/get_color() + return COLOR_SILVER diff --git a/code/modules/mob/living/simple_animal/genetics/animal_genes/diet.dm b/code/modules/mob/living/simple_animal/genetics/animal_genes/diet.dm new file mode 100644 index 00000000000..cb9389f3741 --- /dev/null +++ b/code/modules/mob/living/simple_animal/genetics/animal_genes/diet.dm @@ -0,0 +1,66 @@ +/datum/animal_gene/diet + abstract_type = /datum/animal_gene/diet + exclusion_group = GENE_GROUP_DIET + var/list/added_foods = list() + var/list/removed_foods = list() + +/datum/animal_gene/diet/apply_to(mob/living/simple_animal/hostile/target) + if(..()) + return + if(!target.food_type) + target.food_type = list() + for(var/F in removed_foods) + target.food_type -= F + for(var/F in added_foods) + if(!(F in target.food_type)) + target.food_type += F + if(target.ai_controller) + target.ai_controller.set_blackboard_key(BB_BASIC_FOODS, typecacheof(target.food_type)) + +/datum/animal_gene/diet/remove_from(mob/living/simple_animal/hostile/target) + if(!..()) + return + if(!target.food_type) + return + for(var/F in added_foods) + target.food_type -= F + for(var/F in removed_foods) + if(!(F in target.food_type)) + target.food_type += F + if(target.ai_controller) + target.ai_controller.set_blackboard_key(BB_BASIC_FOODS, typecacheof(target.food_type)) + +/datum/animal_gene/diet/strict_herbivore + name = "Strict Herbivore" + desc = "Will only eat plant matter. Refuses meat entirely." + rarity = 5 + removed_foods = list( + /obj/item/reagent_containers/food/snacks/meat, + /obj/item/reagent_containers/food/snacks/smallrat, + /obj/item/reagent_containers/food/snacks/fish, + ) + +/datum/animal_gene/diet/omnivore + name = "Opportunistic Omnivore" + desc = "Will eat almost anything, including meat and fish." + rarity = 4 + added_foods = list( + /obj/item/reagent_containers/food/snacks/meat, + /obj/item/reagent_containers/food/snacks/smallrat, + /obj/item/reagent_containers/food/snacks/fish, + ) + +/datum/animal_gene/diet/carnivore_instinct + name = "Carnivore Instinct" + desc = "A rare genetic throwback. This animal craves meat above all else." + rarity = 2 + dominant = TRUE + added_foods = list( + /obj/item/reagent_containers/food/snacks/meat, + /obj/item/reagent_containers/food/snacks/smallrat, + /obj/item/reagent_containers/food/snacks/fish, + ) + removed_foods = list( + /obj/item/reagent_containers/food/snacks/produce, + ) + type_whitelist = list(/mob/living/simple_animal/hostile/retaliate/cow) diff --git a/code/modules/mob/living/simple_animal/genetics/animal_genes/docile.dm b/code/modules/mob/living/simple_animal/genetics/animal_genes/docile.dm new file mode 100644 index 00000000000..7244920bef0 --- /dev/null +++ b/code/modules/mob/living/simple_animal/genetics/animal_genes/docile.dm @@ -0,0 +1,26 @@ +/datum/animal_gene/docile + name = "Docile" + desc = "Exceptionally calm. Much easier to tame; won't flee when struck." + rarity = 4 + exclusion_group = GENE_GROUP_TEMPERAMENT + +/datum/animal_gene/docile/apply_to(mob/living/simple_animal/hostile/target) + if(..()) + return + target.tame_chance = min(95, target.tame_chance + 30) + target.bonus_tame_chance += 10 + if(target.ai_controller) + target.ai_controller.remove_subtree(/datum/ai_planning_subtree/find_nearest_thing_which_attacked_me_to_flee) + target.ai_controller.remove_subtree(/datum/ai_planning_subtree/flee_target) + target.ai_controller.remove_subtree(/datum/ai_planning_subtree/aggro_find_target) + target.ai_controller.remove_subtree(/datum/ai_planning_subtree/basic_melee_attack_subtree) + +/datum/animal_gene/docile/remove_from(mob/living/simple_animal/hostile/target) + if(!..()) + return + target.tame_chance = max(0, target.tame_chance - 30) + target.bonus_tame_chance -= 10 + if(target.ai_controller) + target.ai_controller.add_subtree_at(/datum/ai_planning_subtree/find_nearest_thing_which_attacked_me_to_flee) + target.ai_controller.add_subtree_at(/datum/ai_planning_subtree/flee_target) + diff --git a/code/modules/mob/living/simple_animal/genetics/animal_genes/dominant_lineage.dm b/code/modules/mob/living/simple_animal/genetics/animal_genes/dominant_lineage.dm new file mode 100644 index 00000000000..a1600abf26d --- /dev/null +++ b/code/modules/mob/living/simple_animal/genetics/animal_genes/dominant_lineage.dm @@ -0,0 +1,20 @@ +/datum/animal_gene/dominant_lineage + name = "Dominant Lineage" + desc = "A powerful hereditary force that pulls offspring traits strongly toward one bloodline." + rarity = 3 // quite rare, strong selective effect + exclusion_group = GENE_GROUP_PROGENY + intensity_min = 1 + intensity_max = 10 + /// which parent this gene favors, set at creation, persists through inheritance + var/favored_side = LINEAGE_MOTHER + +/datum/animal_gene/dominant_lineage/New() + ..() + favored_side = pick(LINEAGE_MOTHER, LINEAGE_FATHER) + +/datum/animal_gene/dominant_lineage/breed_with(datum/animal_gene/other) + . = ..() + // Preserve the favored side through inheritance rather than re-rolling it + var/datum/animal_gene/dominant_lineage/offspring = . + offspring.favored_side = favored_side + return offspring diff --git a/code/modules/mob/living/simple_animal/genetics/animal_genes/fat.dm b/code/modules/mob/living/simple_animal/genetics/animal_genes/fat.dm new file mode 100644 index 00000000000..57729e85dff --- /dev/null +++ b/code/modules/mob/living/simple_animal/genetics/animal_genes/fat.dm @@ -0,0 +1,33 @@ +/datum/animal_gene/fat + name = "Fat" + desc = "Heavyset build. More meat when butchered, but slower." + rarity = 8 + exclusion_group = GENE_GROUP_BODY_SIZE + intensity_min = 11 + intensity_max = 18 + +/datum/animal_gene/fat/apply_to(mob/living/simple_animal/hostile/target) + if(..()) + return + target.genetic_butcher_scale = intensity + var/hp_bonus = round(initial(target.maxHealth) * (intensity - 1.0) * 0.5) + target.maxHealth += hp_bonus + target.health = min(target.health + hp_bonus, target.maxHealth) + var/delay_penalty = round((intensity - 1.0) * 5) + target.genetic_speed_delta += delay_penalty + if(target.ai_controller) + target.ai_controller.movement_delay = max(1, target.ai_controller.movement_delay + delay_penalty) + target.move_to_delay = max(1, target.move_to_delay + delay_penalty) + +/datum/animal_gene/fat/remove_from(mob/living/simple_animal/hostile/target) + if(!..()) + return + target.genetic_butcher_scale = initial(target.genetic_butcher_scale) + var/hp_bonus = round(initial(target.maxHealth) * (intensity - 1.0) * 0.5) + target.maxHealth -= hp_bonus + target.health = min(target.health, target.maxHealth) + var/delay_penalty = round((intensity - 1.0) * 5) + target.genetic_speed_delta -= delay_penalty + if(target.ai_controller) + target.ai_controller.movement_delay -= delay_penalty + target.move_to_delay = max(1, target.move_to_delay - delay_penalty) diff --git a/code/modules/mob/living/simple_animal/genetics/animal_genes/fecundity.dm b/code/modules/mob/living/simple_animal/genetics/animal_genes/fecundity.dm new file mode 100644 index 00000000000..46bf90924da --- /dev/null +++ b/code/modules/mob/living/simple_animal/genetics/animal_genes/fecundity.dm @@ -0,0 +1,31 @@ +/datum/animal_gene/fecundity + name = "Fecundity" + desc = "Affects reproductive drive, altering the cooldown between breeding cycles." + rarity = 6 + exclusion_group = GENE_GROUP_BREEDING + intensity_min = 1 + intensity_max = 10 + +/datum/animal_gene/fecundity/apply_to(mob/living/simple_animal/target) + . = ..() + if(.) + return + var/datum/component/breed/breed_component = target.GetComponent(/datum/component/breed) + if(!breed_component) + return + // Intensity ranges 0.1–1.0; at max intensity, timer is halved. At min, barely changed. + // Multiplier lands between 0.5 (max) and ~0.95 (min) + var/multiplier = 1.0 - (intensity * 0.5) + breed_component.breed_timer = max(breed_component.breed_timer * multiplier, 10 SECONDS) + +/datum/animal_gene/fecundity/remove_from(mob/living/simple_animal/target) + . = ..() + if(!.) + return + var/datum/component/breed/breed_component = target.GetComponent(/datum/component/breed) + if(!breed_component) + return + // Reverse the multiplier to restore the original timer + var/multiplier = 1.0 - (intensity * 0.5) + if(multiplier > 0) + breed_component.breed_timer = breed_component.breed_timer / multiplier diff --git a/code/modules/mob/living/simple_animal/genetics/animal_genes/frail.dm b/code/modules/mob/living/simple_animal/genetics/animal_genes/frail.dm new file mode 100644 index 00000000000..885106959bc --- /dev/null +++ b/code/modules/mob/living/simple_animal/genetics/animal_genes/frail.dm @@ -0,0 +1,21 @@ +/datum/animal_gene/frail + name = "Frail" + desc = "Delicate constitution. Lower max health." + rarity = 6 + exclusion_group = GENE_GROUP_CONSTITUTION + intensity_min = 1 + intensity_max = 5 + +/datum/animal_gene/frail/apply_to(mob/living/simple_animal/hostile/target) + if(..()) + return + var/loss = round(initial(target.maxHealth) * intensity) + target.maxHealth = max(5, target.maxHealth - loss) + target.health = min(target.health, target.maxHealth) + +/datum/animal_gene/frail/remove_from(mob/living/simple_animal/hostile/target) + if(!..()) + return + var/loss = round(initial(target.maxHealth) * intensity) + target.maxHealth += loss + target.health = min(target.health, target.maxHealth) diff --git a/code/modules/mob/living/simple_animal/genetics/animal_genes/glowing_undercoat.dm b/code/modules/mob/living/simple_animal/genetics/animal_genes/glowing_undercoat.dm new file mode 100644 index 00000000000..20b270557fa --- /dev/null +++ b/code/modules/mob/living/simple_animal/genetics/animal_genes/glowing_undercoat.dm @@ -0,0 +1,13 @@ +/datum/animal_gene/glowing_undercoat + name = "Glowing Undercoat" + exclusion_group = GENE_GROUP_EMISSIVE + dominant = TRUE + emergence_chance = 5 + rarity = 1 + gene_flags = GENE_FLAG_INTRINSIC | GENE_FLAG_EMERGENCE + type_whitelist = list( + /mob/living/simple_animal/hostile/retaliate/saiga, + /mob/living/simple_animal/hostile/retaliate/saigabuck, + /mob/living/simple_animal/hostile/retaliate/saiga/saigakid, + /mob/living/simple_animal/hostile/retaliate/saiga/saigakid/boy, + ) diff --git a/code/modules/mob/living/simple_animal/genetics/animal_genes/hardy.dm b/code/modules/mob/living/simple_animal/genetics/animal_genes/hardy.dm new file mode 100644 index 00000000000..7471b409cb3 --- /dev/null +++ b/code/modules/mob/living/simple_animal/genetics/animal_genes/hardy.dm @@ -0,0 +1,22 @@ +/datum/animal_gene/hardy + name = "Hardy" + desc = "Robust constitution. Significantly higher max health." + rarity = 6 + + exclusion_group = GENE_GROUP_CONSTITUTION + intensity_min = 2 + intensity_max = 6 + +/datum/animal_gene/hardy/apply_to(mob/living/simple_animal/hostile/target) + if(..()) + return + var/bonus = round(initial(target.maxHealth) * intensity) + target.maxHealth += bonus + target.health = min(target.health + bonus, target.maxHealth) + +/datum/animal_gene/hardy/remove_from(mob/living/simple_animal/hostile/target) + if(!..()) + return + var/bonus = round(initial(target.maxHealth) * intensity) + target.maxHealth -= bonus + target.health = min(target.health, target.maxHealth) diff --git a/code/modules/mob/living/simple_animal/genetics/animal_genes/hide.dm b/code/modules/mob/living/simple_animal/genetics/animal_genes/hide.dm new file mode 100644 index 00000000000..faca6d6c179 --- /dev/null +++ b/code/modules/mob/living/simple_animal/genetics/animal_genes/hide.dm @@ -0,0 +1,30 @@ +/datum/animal_gene/hide + abstract_type = /datum/animal_gene/hide + exclusion_group = GENE_GROUP_HIDE + var/list/armor_covered = list() + +/datum/animal_gene/hide/apply_to(mob/living/simple_animal/hostile/target) + if(..()) + return + ADD_TRAIT(target, TRAIT_ANIMAL_NATURAL_ARMOR, GENETICS_TRAIT) + +/datum/animal_gene/hide/remove_from(mob/living/simple_animal/hostile/target) + if(!..()) + return + REMOVE_TRAIT(target, TRAIT_ANIMAL_NATURAL_ARMOR, GENETICS_TRAIT) + +/datum/animal_gene/hide/thick_hide + name = "Thick Hide" + desc = "Dense skin. Absorbs melee and slashing damage." + rarity = 4 + intensity_min = 50 + intensity_max = 200 + armor_covered = list("stab", "slash") + +/datum/animal_gene/hide/ironhide + name = "Ironhide" + desc = "Unnaturally dense flesh. Resists most physical damage types." + rarity = 2 + intensity_min = 150 + intensity_max = 400 + armor_covered = list("stab", "slash", "piercing") diff --git a/code/modules/mob/living/simple_animal/genetics/animal_genes/lean.dm b/code/modules/mob/living/simple_animal/genetics/animal_genes/lean.dm new file mode 100644 index 00000000000..9a6c5cb21de --- /dev/null +++ b/code/modules/mob/living/simple_animal/genetics/animal_genes/lean.dm @@ -0,0 +1,33 @@ +/datum/animal_gene/lean + name = "Lean" + desc = "Slender build. Less meat when butchered, but slightly quicker." + rarity = 8 + exclusion_group = GENE_GROUP_BODY_SIZE + intensity_min = 3 + intensity_max = 8 + +/datum/animal_gene/lean/apply_to(mob/living/simple_animal/hostile/target) + if(..()) + return + target.genetic_butcher_scale = intensity + var/hp_loss = round(initial(target.maxHealth) * (1.0 - intensity) * 0.4) + target.maxHealth = max(5, target.maxHealth - hp_loss) + target.health = min(target.health, target.maxHealth) + var/delay_bonus = round((1.0 - intensity) * 3) + target.genetic_speed_delta -= delay_bonus + if(target.ai_controller) + target.ai_controller.movement_delay = max(1, target.ai_controller.movement_delay - delay_bonus) + target.move_to_delay = max(1, target.move_to_delay - delay_bonus) + +/datum/animal_gene/lean/remove_from(mob/living/simple_animal/hostile/target) + if(!..()) + return + target.genetic_butcher_scale = initial(target.genetic_butcher_scale) + var/hp_loss = round(initial(target.maxHealth) * (1.0 - intensity) * 0.4) + target.maxHealth += hp_loss + target.health = min(target.health, target.maxHealth) + var/delay_bonus = round((1.0 - intensity) * 3) + target.genetic_speed_delta += delay_bonus + if(target.ai_controller) + target.ai_controller.movement_delay += delay_bonus + target.move_to_delay += delay_bonus diff --git a/code/modules/mob/living/simple_animal/genetics/animal_genes/productive.dm b/code/modules/mob/living/simple_animal/genetics/animal_genes/productive.dm new file mode 100644 index 00000000000..e4e7c51ad04 --- /dev/null +++ b/code/modules/mob/living/simple_animal/genetics/animal_genes/productive.dm @@ -0,0 +1,14 @@ +/datum/animal_gene/productive + name = "Productive" + desc = "High-yield. Produces milk, wool, or eggs more frequently." + rarity = 4 + +/datum/animal_gene/productive/apply_to(mob/living/simple_animal/hostile/target) + if(..()) + return + ADD_TRAIT(target, TRAIT_ANIMAL_PRODUCTIVE, GENETICS_TRAIT) + +/datum/animal_gene/productive/remove_from(mob/living/simple_animal/hostile/target) + if(!..()) + return + REMOVE_TRAIT(target, TRAIT_ANIMAL_PRODUCTIVE, GENETICS_TRAIT) diff --git a/code/modules/mob/living/simple_animal/genetics/animal_genes/prolific.dm b/code/modules/mob/living/simple_animal/genetics/animal_genes/prolific.dm new file mode 100644 index 00000000000..426491ee099 --- /dev/null +++ b/code/modules/mob/living/simple_animal/genetics/animal_genes/prolific.dm @@ -0,0 +1,70 @@ +/datum/animal_gene/prolific + name = "Prolific" + desc = "Grants a chance to birth multiple offspring in a single litter." + rarity = 4 + exclusion_group = GENE_GROUP_PROGENY + intensity_min = 1 + intensity_max = 10 + +/datum/animal_gene/prolific/apply_to(mob/living/simple_animal/target) + . = ..() + if(.) + return + var/datum/component/breed/breed_component = target.GetComponent(/datum/component/breed) + if(!breed_component) + return + // Override the birth callback to inject our multi-birth logic + var/datum/callback/old_override = breed_component.override_baby + breed_component.override_baby = CALLBACK(src, PROC_REF(prolific_birth), target, breed_component, old_override) + +/datum/animal_gene/prolific/remove_from(mob/living/simple_animal/target) + . = ..() + if(!.) + return + var/datum/component/breed/breed_component = target.GetComponent(/datum/component/breed) + if(!breed_component) + return + // Clear our override; if there was a prior override we stored it, but restoring + // arbitrary callback chains cleanly is complex, nulling is safest here. + // If your codebase commonly stacks overrides, consider a callback list instead. + breed_component.override_baby = null + +/// Handles the actual multi-birth. Spawns between 1 and max_litter babies, +/// weighted toward smaller litters, then fires post_birth for each. +/datum/animal_gene/prolific/proc/prolific_birth(mob/living/simple_animal/mother, datum/component/breed/breed_comp, datum/callback/old_override) + // At intensity 1.0 (max): ~60% chance of extra kids, up to 4 total + // At intensity 0.1 (min): ~6% chance of even one extra, max 2 total + var/extra_chance = intensity * 60 // 6%–60% + var/max_litter = max(1, round(intensity * 4)) // 1–4 extras on top of the base baby + + var/extras = 0 + for(var/i in 1 to max_litter) + if(prob(extra_chance)) + extras++ + else + break + + var/total = 1 + extras + var/turf/loc = get_turf(mother) + + if(old_override) + // Respect any existing override for the base baby + old_override.Invoke() + else + // Spawn the base baby normally + var/picked = pickweight(breed_comp.baby_path) + var/mob/living/base_baby = new picked(loc) + SEND_SIGNAL(mother, COMSIG_FRIENDSHIP_PASS_FRIENDSHIP, base_baby) + SEND_SIGNAL(mother, COMSIG_HAPPINESS_PASS_HAPPINESS, base_baby) + breed_comp.post_birth?.Invoke(base_baby, null) + + // Spawn any extras + for(var/i in 1 to extras) + var/picked = pickweight(breed_comp.baby_path) + var/mob/living/extra_baby = new picked(loc) + SEND_SIGNAL(mother, COMSIG_FRIENDSHIP_PASS_FRIENDSHIP, extra_baby) + SEND_SIGNAL(mother, COMSIG_HAPPINESS_PASS_HAPPINESS, extra_baby) + breed_comp.post_birth?.Invoke(extra_baby, null) + + if(total > 1) + mother.visible_message(span_notice("[mother] gives birth to a litter of [total]!")) diff --git a/code/modules/mob/living/simple_animal/genetics/animal_genes/slugish.dm b/code/modules/mob/living/simple_animal/genetics/animal_genes/slugish.dm new file mode 100644 index 00000000000..8e78e11342b --- /dev/null +++ b/code/modules/mob/living/simple_animal/genetics/animal_genes/slugish.dm @@ -0,0 +1,25 @@ +/datum/animal_gene/sluggish + name = "Sluggish" + desc = "Slower than average. Easy to herd." + rarity = 8 + exclusion_group = GENE_GROUP_SPEED + intensity_min = 10 + intensity_max = 50 + +/datum/animal_gene/sluggish/apply_to(mob/living/simple_animal/hostile/target) + if(..()) + return + var/penalty = round(intensity) + target.genetic_speed_delta += penalty + if(target.ai_controller) + target.ai_controller.movement_delay += penalty + target.move_to_delay += penalty + +/datum/animal_gene/sluggish/remove_from(mob/living/simple_animal/hostile/target) + if(!..()) + return + var/penalty = round(intensity) + target.genetic_speed_delta -= penalty + if(target.ai_controller) + target.ai_controller.movement_delay -= penalty + target.move_to_delay -= penalty diff --git a/code/modules/mob/living/simple_animal/genetics/animal_genes/swift.dm b/code/modules/mob/living/simple_animal/genetics/animal_genes/swift.dm new file mode 100644 index 00000000000..0ad59385263 --- /dev/null +++ b/code/modules/mob/living/simple_animal/genetics/animal_genes/swift.dm @@ -0,0 +1,26 @@ +/datum/animal_gene/swift + name = "Swift" + desc = "Faster than average. Harder to herd or catch." + rarity = 6 + exclusion_group = GENE_GROUP_SPEED + intensity_min = 5 + intensity_max = 30 + +/datum/animal_gene/swift/apply_to(mob/living/simple_animal/hostile/target) + if(..()) + return + var/reduction = round(intensity) + target.genetic_speed_delta -= reduction + if(target.ai_controller) + target.ai_controller.movement_delay = max(1, target.ai_controller.movement_delay - reduction) + + target.move_to_delay = max(1, target.move_to_delay - reduction) + +/datum/animal_gene/swift/remove_from(mob/living/simple_animal/hostile/target) + if(!..()) + return + var/reduction = round(intensity) + target.genetic_speed_delta += reduction + if(target.ai_controller) + target.ai_controller.movement_delay += reduction + target.move_to_delay += reduction diff --git a/code/modules/mob/living/simple_animal/genetics/animal_genes/undercoat.dm b/code/modules/mob/living/simple_animal/genetics/animal_genes/undercoat.dm new file mode 100644 index 00000000000..3cbe779d42b --- /dev/null +++ b/code/modules/mob/living/simple_animal/genetics/animal_genes/undercoat.dm @@ -0,0 +1,59 @@ +/datum/animal_gene/undercoat + name = "Undercoat" + exclusion_group = GENE_GROUP_UNDERCOAT + dominant = TRUE + rarity = 10 + abstract_type = /datum/animal_gene/undercoat + gene_flags = GENE_FLAG_INTRINSIC + type_whitelist = list( + /mob/living/simple_animal/hostile/retaliate/saiga, + /mob/living/simple_animal/hostile/retaliate/saigabuck, + /mob/living/simple_animal/hostile/retaliate/saiga/saigakid, + /mob/living/simple_animal/hostile/retaliate/saiga/saigakid/boy, + ) + +/datum/animal_gene/undercoat/breed_with(datum/animal_gene/other) + if(!other) + return new type() + if(prob(50)) + return new type() + return new other.type() + +/datum/animal_gene/undercoat/proc/get_color() + return COLOR_WHITE + +/datum/animal_gene/undercoat/white + name = "White Undercoat" +/datum/animal_gene/undercoat/white/get_color() + return COLOR_WHITE + +/datum/animal_gene/undercoat/gray + name = "Gray Undercoat" +/datum/animal_gene/undercoat/gray/get_color() + return COLOR_GRAY + +/datum/animal_gene/undercoat/black + name = "Black Undercoat" +/datum/animal_gene/undercoat/black/get_color() + return COLOR_ALMOST_BLACK + +/datum/animal_gene/undercoat/brown + name = "Brown Undercoat" +/datum/animal_gene/undercoat/brown/get_color() + return COLOR_DARK_BROWN + +/datum/animal_gene/undercoat/chestnut + name = "Chestnut Undercoat" +/datum/animal_gene/undercoat/chestnut/get_color() + return COLOR_DARK_ORANGE + +/datum/animal_gene/undercoat/silver_dapple + name = "Silver Dapple Undercoat" + gene_flags = GENE_FLAG_INTRINSIC | GENE_FLAG_EMERGENCE + required_parent_genes = list( + /datum/animal_gene/undercoat/black, + /datum/animal_gene/undercoat/chestnut + ) + +/datum/animal_gene/undercoat/silver_dapple/get_color() + return COLOR_SILVER diff --git a/code/modules/mob/living/simple_animal/genetics/animal_genetics.dm b/code/modules/mob/living/simple_animal/genetics/animal_genetics.dm new file mode 100644 index 00000000000..835ff96bdd1 --- /dev/null +++ b/code/modules/mob/living/simple_animal/genetics/animal_genetics.dm @@ -0,0 +1,395 @@ +GLOBAL_LIST_INIT(all_animal_genes_weighted, generate_animaL_genes()) +/proc/generate_animaL_genes() + . = list() + for(var/datum/animal_gene/gene as anything in subtypesof(/datum/animal_gene)) + if(IS_ABSTRACT(gene)) + continue + .[gene] = initial(gene.rarity) + return . + +/datum/animal_genetics + var/datum/weakref/owner_ref = null + var/list/datum/animal_gene/genes = list() + var/list/guaranteed_genes = list() // list of gene TYPES that always roll on creation, e.g. list(/datum/animal_gene/coat_color/brown) + +/datum/animal_genetics/New(mob/living/simple_animal/owner) + owner_ref = WEAKREF(owner) + +/datum/animal_genetics/Destroy() + for(var/datum/animal_gene/G in genes) + qdel(G) + genes = list() + owner_ref = null + return ..() + +/datum/animal_genetics/proc/roll_guaranteed_genes() + var/list/true_genes = list() + for(var/datum/animal_gene/gene_type as anything in guaranteed_genes) + if(IS_ABSTRACT(gene_type)) + var/list/picked = list() + for(var/datum/animal_gene/gene as anything in subtypesof(gene_type)) + if((gene.gene_flags & GENE_FLAG_EXLUDE_WILD) || (gene.gene_flags & GENE_FLAG_EMERGENCE)) + continue + picked[gene] = initial(gene.rarity) + true_genes |= pickweight(picked) + continue + true_genes |= gene_type + + for(var/gene_type in true_genes) + var/datum/animal_gene/G = new gene_type() + if(!_insert_gene(G)) + qdel(G) + refresh() + +/datum/animal_genetics/proc/should_express(datum/animal_gene/G) + var/mob/living/simple_animal/owner = owner_ref?.resolve() + if(!owner || !G.allowed_for(owner)) + return FALSE + if(G.dominant) + return TRUE + return G.recessive_state == RECESSIVE_EXPRESSED + +/datum/animal_genetics/proc/_insert_gene(datum/animal_gene/new_gene) + if(!((new_gene.gene_flags & GENE_FLAG_INTRINSIC) || (new_gene.gene_flags & GENE_FLAG_UNCOUNTED))) + if(get_gene_count() >= GENETICS_MAX_GENES) + return FALSE + if(new_gene.exclusion_group) + for(var/datum/animal_gene/existing in genes) + if(existing.exclusion_group == new_gene.exclusion_group) + genes -= existing + qdel(existing) + break + genes += new_gene + return TRUE + +/datum/animal_genetics/proc/refresh() + var/mob/living/simple_animal/owner = owner_ref?.resolve() + if(!owner) + return + for(var/datum/animal_gene/G in genes) + if(should_express(G)) + owner.remove_gene(G) + owner.apply_gene(G) + +/datum/animal_genetics/proc/add_gene(datum/animal_gene/new_gene) + if(!_insert_gene(new_gene)) + return FALSE + refresh() + return TRUE + +/datum/animal_genetics/proc/remove_gene(datum/animal_gene/target_gene) + if(!(target_gene in genes)) + return FALSE + var/mob/living/simple_animal/owner = owner_ref?.resolve() + genes -= target_gene + owner.remove_gene(target_gene) + qdel(target_gene) + refresh() + return TRUE + +/datum/animal_genetics/proc/get_natural_armor_for_type(type_str) + var/total = 0 + for(var/datum/animal_gene/hide/H in genes) + if(should_express(H) && (type_str in H.armor_covered)) + total += round(H.intensity) + return total + +/datum/animal_genetics/proc/get_gene_count() + var/count = 0 + for(var/datum/animal_gene/G in genes) + if(!((G.gene_flags & GENE_FLAG_INTRINSIC) || (G.gene_flags & GENE_FLAG_UNCOUNTED))) + count++ + return count + +/datum/animal_genetics/proc/get_gene_names() + var/list/names = list() + for(var/datum/animal_gene/G in genes) + names += should_express(G) ? G.name : "[G.name] (recessive)" + return names + +/datum/animal_genetics/proc/get_gene_by_exclusion_group(group) + for(var/datum/animal_gene/G in genes) + if(G.exclusion_group == group) + return G + return null + +/// Build this parent's contribution for an inheritance pass. +/// Returns an associative list of exclusion_group (or type path for ungrouped) -> gene datum. +/// Whether a gene is offered at all depends on dominant/recessive pass chances. +/// Dominant genes are more reliably passed; recessives need selection pressure from both sides. +/datum/animal_genetics/proc/_build_allele_pool() + var/list/pool = list() + for(var/datum/animal_gene/G in genes) + if((G.gene_flags & GENE_FLAG_INTRINSIC)) + var/key = G.exclusion_group ? G.exclusion_group : "[G.type]" + pool[key] = G + continue + var/pass_chance = G.dominant ? GENETICS_DOMINANT_PASS_CHANCE : GENETICS_RECESSIVE_PASS_CHANCE + if(!prob(pass_chance)) + continue + var/key = G.exclusion_group ? G.exclusion_group : "[G.type]" + if(pool[key]) + var/datum/animal_gene/existing = pool[key] + if(G.intensity > existing.intensity) + pool[key] = G + else + pool[key] = G + return pool + +/// Merge two allele pools (mother and father) into bred offspring genes. +/// Where both parents offer the same trait, breed_with() averages intensities biased upward. +/// Where only one parent offers a trait, it passes through unpaired with minor noise. +/datum/animal_genetics/proc/_breed_pools(list/mother_pool, list/father_pool, mob/living/simple_animal/mother, mob/living/simple_animal/father) + // Check if either parent carries an expressed dominant lineage gene + var/lineage_weight = 0.5 // default: equal chance from either side + var/datum/animal_gene/dominant_lineage/lineage_gene = null + + for(var/datum/animal_gene/dominant_lineage/G in genes) // mother's genes + if(should_express(G)) + lineage_gene = G + break + if(!lineage_gene && father?.genetics) + for(var/datum/animal_gene/dominant_lineage/G in father.genetics.genes) + if(father.genetics.should_express(G)) + lineage_gene = G + break + + if(lineage_gene) + // intensity 0.1–1.0 maps to a skew of 0.05–0.5 away from center + // at max intensity: 100% from favored side (weight = 0 or 1) + // at min intensity: 55/45 split + var/skew = lineage_gene.intensity * 0.5 + lineage_weight = (lineage_gene.favored_side == LINEAGE_MOTHER) ? (0.5 + skew) : (0.5 - skew) + + var/list/datum/animal_gene/result = list() + var/list/all_keys = list() + for(var/key in mother_pool) + all_keys |= key + for(var/key in father_pool) + all_keys |= key + + for(var/key in all_keys) + var/datum/animal_gene/MG = mother_pool[key] + var/datum/animal_gene/FG = father_pool[key] + var/datum/animal_gene/offspring + + if(MG && FG) + // Both parents have this trait, use lineage weight to pick whose + // intensity anchors the breed_with call, rather than always averaging evenly + if(prob(lineage_weight * 100)) + offspring = MG.breed_with(FG) + else + offspring = FG.breed_with(MG) + if(!offspring.dominant) + offspring.recessive_state = RECESSIVE_EXPRESSED + else + var/datum/animal_gene/source = MG ? MG : FG + // If the unpaired gene is from the non-favored side, it has a chance to be dropped + // entirely, making the favored lineage even more dominant at high intensity + if(lineage_gene) + var/is_mothers = MG != null + var/favors_mother = lineage_gene.favored_side == LINEAGE_MOTHER + var/from_favored = (is_mothers == favors_mother) + if(!from_favored && !prob(lineage_weight * 100)) + continue // discard this gene entirely, the favored bloodline crowds it out + offspring = source.breed_with(null) + if(!offspring.dominant) + offspring.recessive_state = RECESSIVE_CARRIED + result += offspring + return result + +/datum/animal_genetics/proc/_check_emergence_requirements(datum/animal_gene/candidate, list/mother_pool, list/father_pool) + if(!candidate.required_parent_genes || !length(candidate.required_parent_genes)) + return TRUE + // Check mother's pool for at least one match + var/mother_match = FALSE + for(var/key in mother_pool) + var/datum/animal_gene/G = mother_pool[key] + if(is_type_in_list(G, candidate.required_parent_genes)) + mother_match = TRUE + break + if(!mother_match) + return FALSE + // Check father's pool for at least one match + var/father_match = FALSE + for(var/key in father_pool) + var/datum/animal_gene/G = father_pool[key] + if(is_type_in_list(G, candidate.required_parent_genes)) + father_match = TRUE + break + return father_match + +/datum/animal_genetics/proc/inherit_to(mob/living/simple_animal/baby, mob/living/simple_animal/father) + if(!istype(baby.genetics)) + baby.genetics = new /datum/animal_genetics(baby) + var/list/mother_pool = _build_allele_pool() + var/list/father_pool = istype(father?.genetics) ? father.genetics._build_allele_pool() : list() + var/list/datum/animal_gene/candidates = _breed_pools(mother_pool, father_pool) + // Shuffle so no ordering bias when inserting up to the gene cap + candidates = shuffle(candidates) + for(var/datum/animal_gene/G in candidates) + if(!baby.genetics._insert_gene(G)) + qdel(G) + // Mutation pass on remaining open slots for a chance at +1 + var/open_slots = GENETICS_MAX_GENES - baby.genetics.get_gene_count() + if(open_slots) + if(prob(GENETICS_MUTATION_CHANCE)) + var/datum/animal_gene/mutant = genetics_roll_mutation(baby, FALSE, mother_pool, father_pool) + if(mutant && !baby.genetics._insert_gene(mutant)) + qdel(mutant) + + var/list/emergence_candidates = list() + for(var/gene_type in GLOB.all_animal_genes_weighted) + var/datum/animal_gene/tmp = new gene_type() + if(!(tmp.gene_flags & GENE_FLAG_EMERGENCE)) + qdel(tmp) + continue + if(!tmp.allowed_for(baby)) + qdel(tmp) + continue + if(_check_emergence_requirements(tmp, mother_pool, father_pool)) + emergence_candidates[gene_type] = GLOB.all_animal_genes_weighted[gene_type] + qdel(tmp) + + if(length(emergence_candidates) && prob(GENETICS_EMERGENCE_CHANCE)) + var/datum/animal_gene/path = pickweight(emergence_candidates) + var/datum/animal_gene/emergent = new path() + if(!baby.genetics._insert_gene(emergent)) + qdel(emergent) + + baby.genetics.refresh() + var/list/expressed = list() + for(var/datum/animal_gene/G in baby.genetics.genes) + if(baby.genetics.should_express(G)) + expressed += G.name + if(length(expressed)) + baby.visible_message(span_notice("[baby] is born showing distinct traits: [english_list(expressed)].")) + +/datum/animal_genetics/proc/copy_to(mob/living/simple_animal/target) + if(!target.genetics) + target.genetics = new /datum/animal_genetics(target) + else + for(var/datum/animal_gene/G in target.genetics.genes) + G.remove_from(target) + qdel(G) + target.genetics.genes = list() + + for(var/datum/animal_gene/G in genes) + var/datum/animal_gene/copy = new G.type() + copy.dominant = G.dominant + copy.intensity = G.intensity + copy.recessive_state = G.recessive_state + copy.gene_flags = G.gene_flags + if(!target.genetics._insert_gene(copy)) + qdel(copy) + target.genetics.refresh() + +/proc/genetics_roll_mutation(mob/living/simple_animal/hostile/target, wild = FALSE, list/mother_pool = null, list/father_pool = null) + var/list/valid = list() + for(var/gene_type in GLOB.all_animal_genes_weighted) + var/datum/animal_gene/tmp = new gene_type() + if(wild && (tmp.gene_flags & GENE_FLAG_EXLUDE_WILD)) + qdel(tmp) + continue + // Emergence-flagged genes are never rolled in the wild + if(wild && (tmp.gene_flags & GENE_FLAG_EMERGENCE)) + qdel(tmp) + continue + // If emergence is required during breeding, check parent pools + if((tmp.gene_flags & GENE_FLAG_EMERGENCE) && (mother_pool || father_pool)) + if(!_check_emergence_requirements(tmp, mother_pool || list(), father_pool || list())) + qdel(tmp) + continue + if(tmp.emergence_chance && !prob(tmp.emergence_chance)) + qdel(tmp) + continue + if(tmp.allowed_for(target)) + valid[gene_type] = GLOB.all_animal_genes_weighted[gene_type] + qdel(tmp) + if(!length(valid)) + return null + var/datum/animal_gene/new_gene = pickweight(valid) + return new new_gene() + +/proc/_check_emergence_requirements(datum/animal_gene/candidate, list/mother_pool, list/father_pool) + if(!candidate.required_parent_genes || !length(candidate.required_parent_genes)) + return TRUE + + var/list/remaining = candidate.required_parent_genes.Copy() + + for(var/key in mother_pool) + var/datum/animal_gene/G = mother_pool[key] + for(var/req_type in remaining) + if(istype(G, req_type)) + remaining -= req_type + break + + for(var/key in father_pool) + var/datum/animal_gene/G = father_pool[key] + for(var/req_type in remaining) + if(istype(G, req_type)) + remaining -= req_type + break + + return !length(remaining) + +/mob/living/simple_animal/proc/roll_initial_genetics(max_genes = 2, intensity_bound_cap = 0.4, recessive_bias = 30) + if(!genetics || ispath(genetics)) + genetics = new /datum/animal_genetics(src) + + var/attempts = 0 + while(genetics.get_gene_count() < max_genes && attempts < max_genes * 4) + attempts++ + + var/datum/animal_gene/G = genetics_roll_mutation(src, TRUE) + if(!G) + continue + + G.intensity = rand(G.intensity_min, round(G.intensity_max * intensity_bound_cap, 1)) * 0.1 + + if(prob(recessive_bias)) + G.dominant = FALSE + + if(!G.dominant) + G.recessive_state = RECESSIVE_CARRIED + + if(!genetics._insert_gene(G)) + qdel(G) + + genetics.refresh() + +/mob/living/simple_animal/proc/debug_apply_max_genetics() + if(!genetics || ispath(genetics)) + genetics = new /datum/animal_genetics(src) + + var/datum/animal_gene/swift/swift = new() + swift.intensity = initial(swift.intensity_max) * 0.1 + if(!genetics._insert_gene(swift)) + qdel(swift) + + var/datum/animal_gene/lean/lean = new() + lean.intensity = initial(lean.intensity_max) * 0.1 + if(!genetics._insert_gene(lean)) + qdel(lean) + + var/datum/animal_gene/hardy/hardy = new() + hardy.dominant = TRUE + hardy.intensity = initial(hardy.intensity_max) * 0.1 + + if(!genetics._insert_gene(hardy)) + qdel(hardy) + + var/datum/animal_gene/hide/ironhide/ironhide = new() + ironhide.dominant = TRUE + ironhide.intensity = initial(ironhide.intensity_max) * 0.1 + if(!genetics._insert_gene(ironhide)) + qdel(ironhide) + + genetics.refresh() + +/mob/living/simple_animal/proc/debug_breed_with(mob/living/simple_animal/father) + var/mob/living/simple_animal/baby = new type(loc) + baby.name = "Debug [name]" + if(genetics) + genetics.inherit_to(baby, father) + return baby diff --git a/code/modules/mob/living/simple_animal/genetics/genetic_types.dm b/code/modules/mob/living/simple_animal/genetics/genetic_types.dm new file mode 100644 index 00000000000..aa0aab76ea4 --- /dev/null +++ b/code/modules/mob/living/simple_animal/genetics/genetic_types.dm @@ -0,0 +1,5 @@ +/datum/animal_genetics/honse + guaranteed_genes = list(/datum/animal_gene/coat_color) + +/datum/animal_genetics/saiga + guaranteed_genes = list(/datum/animal_gene/coat_color, /datum/animal_gene/undercoat) diff --git a/code/modules/mob/living/simple_animal/hostile/retaliate/farm/chicken.dm b/code/modules/mob/living/simple_animal/hostile/retaliate/farm/chicken.dm index 26c819d49b5..acf8044decb 100644 --- a/code/modules/mob/living/simple_animal/hostile/retaliate/farm/chicken.dm +++ b/code/modules/mob/living/simple_animal/hostile/retaliate/farm/chicken.dm @@ -48,6 +48,8 @@ pooptype = /obj/item/natural/poo/horse happy_funtime_mob = TRUE + generate_genetics = TRUE + var/eggsFertile = TRUE var/body_color var/icon_prefix = "chicken" @@ -131,13 +133,22 @@ /mob/living/simple_animal/hostile/retaliate/chicken/Life() ..() if(SEND_SIGNAL(src, COMSIG_MOB_RETURN_HUNGER) > 0) - production = min(production + 1, 100) + var/productive = 1 + if(HAS_TRAIT(src, TRAIT_ANIMAL_PRODUCTIVE)) + productive *= 3 + production = min(production + productive, 100) /mob/living/simple_animal/hostile/retaliate/chicken/proc/hatch_eggs() for(var/obj/item/reagent_containers/food/snacks/egg/egg in loc) if(!egg.fertile) continue - egg.hatch(src) + var/mob/living/simple_animal/hostile/retaliate/chicken/suprise_father + for(var/mob/living/simple_animal/hostile/retaliate/chicken/potential_father in range(5, src)) + if(potential_father.gender == MALE) + suprise_father = potential_father + break + + egg.hatch(src, suprise_father) qdel(egg) @@ -179,6 +190,7 @@ ai_controller = /datum/ai_controller/basic_controller/chicken/baby chicken_init = FALSE + generate_genetics = FALSE /obj/structure/fluff/nest name = "nest" diff --git a/code/modules/mob/living/simple_animal/hostile/retaliate/farm/cow.dm b/code/modules/mob/living/simple_animal/hostile/retaliate/farm/cow.dm index f7e6c3b71fe..701313ec907 100644 --- a/code/modules/mob/living/simple_animal/hostile/retaliate/farm/cow.dm +++ b/code/modules/mob/living/simple_animal/hostile/retaliate/farm/cow.dm @@ -48,8 +48,7 @@ base_strength = 4 remains_type = /obj/effect/decal/remains/cow happy_funtime_mob = TRUE - - + generate_genetics = TRUE ai_controller = /datum/ai_controller/basic_controller/cow var/can_breed = TRUE @@ -98,9 +97,6 @@ if("idle") return pick('sound/vo/mobs/cow/idle (1).ogg','sound/vo/mobs/cow/idle (2).ogg','sound/vo/mobs/cow/idle (3).ogg','sound/vo/mobs/cow/idle (4).ogg','sound/vo/mobs/cow/idle (5).ogg') -/mob/living/simple_animal/hostile/retaliate/cow/proc/after_birth(mob/living/simple_animal/hostile/retaliate/cow/cowlet/baby, mob/living/partner) - return - /mob/living/simple_animal/hostile/retaliate/cow/simple_limb_hit(zone) switch(zone) if(BODY_ZONE_PRECISE_NOSE, BODY_ZONE_PRECISE_MOUTH) @@ -177,6 +173,7 @@ base_speed = 2 remains_type = /obj/effect/decal/remains/cow happy_funtime_mob = TRUE + generate_genetics = TRUE ai_controller = /datum/ai_controller/basic_controller/cow /mob/living/simple_animal/hostile/retaliate/bull/Initialize() @@ -241,6 +238,7 @@ ai_controller = /datum/ai_controller/basic_controller/cow/baby can_breed = FALSE can_tip = FALSE + generate_genetics = FALSE /mob/living/simple_animal/hostile/retaliate/cow/cowlet/udder_component() return diff --git a/code/modules/mob/living/simple_animal/hostile/retaliate/farm/goat.dm b/code/modules/mob/living/simple_animal/hostile/retaliate/farm/goat.dm index c71b705dc33..1545e698c47 100644 --- a/code/modules/mob/living/simple_animal/hostile/retaliate/farm/goat.dm +++ b/code/modules/mob/living/simple_animal/hostile/retaliate/farm/goat.dm @@ -56,6 +56,7 @@ buckle_lying = FALSE can_buckle = TRUE remains_type = /obj/effect/decal/remains/cow + generate_genetics = TRUE ai_controller = /datum/ai_controller/gote happy_funtime_mob = TRUE @@ -104,9 +105,6 @@ if(can_buckle) AddComponent(/datum/component/riding/gote) -/mob/living/simple_animal/hostile/retaliate/goat/proc/after_birth(mob/living/simple_animal/hostile/retaliate/cow/cowlet/baby, mob/living/partner) - return - /// Called when we attack something in order to piece together the intent of the AI/user and provide desired behavior. The element might be okay here but I'd rather the fluff. /// Goats are really good at beating up plants by taking bites out of them, but we use the default attack for everything else /mob/living/simple_animal/hostile/retaliate/goat/proc/on_pre_attack(datum/source, atom/target) @@ -194,6 +192,7 @@ ai_controller = /datum/ai_controller/gote happy_funtime_mob = TRUE + generate_genetics = TRUE /mob/living/simple_animal/hostile/retaliate/goatmale/Initialize() . = ..() @@ -300,6 +299,7 @@ adult_growth = /mob/living/simple_animal/hostile/retaliate/goat can_buckle = FALSE can_breed = FALSE + generate_genetics = FALSE /mob/living/simple_animal/hostile/retaliate/goat/goatlet/udder_component() return diff --git a/code/modules/mob/living/simple_animal/hostile/retaliate/farm/trufflepig.dm b/code/modules/mob/living/simple_animal/hostile/retaliate/farm/trufflepig.dm index 90ddf3b2015..0e917568704 100644 --- a/code/modules/mob/living/simple_animal/hostile/retaliate/farm/trufflepig.dm +++ b/code/modules/mob/living/simple_animal/hostile/retaliate/farm/trufflepig.dm @@ -172,6 +172,7 @@ ) happy_funtime_mob = TRUE + generate_genetics = TRUE var/hangry_meter = 0 var/random_gender = TRUE var/can_breed = TRUE @@ -190,9 +191,6 @@ CALLBACK(src, PROC_REF(after_birth)),\ ) -/mob/living/simple_animal/hostile/retaliate/trufflepig/proc/after_birth(mob/living/simple_animal/hostile/retaliate/cow/cowlet/baby, mob/living/partner) - return - /mob/living/simple_animal/hostile/retaliate/trufflepig/get_sound(input) switch(input) @@ -305,6 +303,7 @@ name = "truffle piglet" adult_growth = /mob/living/simple_animal/hostile/retaliate/trufflepig/female can_breed = FALSE + generate_genetics = FALSE /mob/living/simple_animal/hostile/retaliate/trufflepig/piglet/Initialize() . = ..() diff --git a/code/modules/mob/living/simple_animal/hostile/retaliate/game/fogbeast.dm b/code/modules/mob/living/simple_animal/hostile/retaliate/game/fogbeast.dm new file mode 100644 index 00000000000..12d97735142 --- /dev/null +++ b/code/modules/mob/living/simple_animal/hostile/retaliate/game/fogbeast.dm @@ -0,0 +1,250 @@ +GLOBAL_LIST_INIT(valid_honse_colors, list("White" = COLOR_WHITE, "Gray" = COLOR_GRAY, "Black" = COLOR_ALMOST_BLACK, "Brown" = COLOR_DARK_BROWN, "Chestnut" = COLOR_DARK_ORANGE)) + +/mob/living/simple_animal/hostile/retaliate/honse + name = "honse mare" + desc = "A distant cousin to the saiga, hailing from the mysterious islands of Kaizoku - rarer, but more strongly valued. Extensively used in the Steppes of Aavnr as pack animals and combat mounts." + icon = 'icons/mob/monster/fogbeast.dmi' + icon_state = "fogbeast" + icon_living = "fogbeast" + icon_dead = "fogbeast_dead" + icon_gib = "saiga_gib" + mob_biotypes = MOB_ORGANIC|MOB_BEAST + emote_see = list("looks around.", "chews some leaves.", "neighs") + speak_chance = 1 + see_in_dark = 6 + move_to_delay = 8 + butcher_results = list( + /obj/item/reagent_containers/food/snacks/meat/steak = 4, + /obj/item/reagent_containers/food/snacks/fat = 2, + /obj/item/natural/hide = 4, + /obj/item/natural/bundle/bone/full = 1 + ) + base_intents = list(/datum/intent/simple/honse) + animal_species = /mob/living/simple_animal/hostile/retaliate/honse/male + health = 380 + maxHealth = 380 + food_type = list( + /obj/item/reagent_containers/food/snacks/produce/grain/wheat, + /obj/item/reagent_containers/food/snacks/produce/grain/oat, + /obj/item/reagent_containers/food/snacks/produce/fruit/apple + ) + tame_chance = 15 + bonus_tame_chance = 15 + footstep_type = FOOTSTEP_MOB_SHOE + pooptype = /obj/item/natural/poo/horse + faction = list("horse") + attack_verb_continuous = "tramples" + attack_verb_simple = "kicks" + melee_damage_lower = 50 + melee_damage_upper = 70 + retreat_distance = 0 + minimum_distance = 10 + base_speed = 15 + base_constitution = 8 + base_strength = 12 + base_endurance = 15 + pixel_x = -8 + attack_sound = list('sound/vo/mobs/saiga/attack (1).ogg','sound/vo/mobs/saiga/attack (2).ogg') + can_buckle = TRUE + buckle_lying = 0 + can_saddle = TRUE + aggressive = TRUE + remains_type = /obj/effect/decal/remains/saiga + ai_controller = /datum/ai_controller/saiga + generate_genetics = TRUE + genetics = /datum/animal_genetics/honse + var/honse_color + var/can_breed = TRUE + +/mob/living/simple_animal/hostile/retaliate/honse/Initialize(mapload) + . = ..() + + if(can_breed) + AddComponent(\ + /datum/component/breed,\ + list(/mob/living/simple_animal/hostile/retaliate/honse),\ + 3 MINUTES,\ + list(/mob/living/simple_animal/hostile/retaliate/honse/kid = 70, /mob/living/simple_animal/hostile/retaliate/honse/kid/male = 30),\ + CALLBACK(src, PROC_REF(after_birth)),\ + ) + +/mob/living/simple_animal/hostile/retaliate/honse/tame + tame = TRUE + +/mob/living/simple_animal/hostile/retaliate/honse/tame/saddled/Initialize() + . = ..() + var/obj/item/natural/saddle/S = new(src) + ssaddle = S + update_icon() + +// BEHAVIORS +/mob/living/simple_animal/hostile/retaliate/honse/update_icon() + cut_overlays() + ..() + if(stat != DEAD) + if(ssaddle) + var/mutable_appearance/saddlet = mutable_appearance(icon, "saddle-above", 4.3) + saddlet.appearance_flags = RESET_ALPHA|RESET_COLOR + add_overlay(saddlet) + saddlet = mutable_appearance(icon, "saddle") + saddlet.appearance_flags = RESET_ALPHA|RESET_COLOR + add_overlay(saddlet) + if(has_buckled_mobs()) + var/mutable_appearance/mounted = mutable_appearance(icon, "[icon_state]_mounted", 4.3) + add_overlay(mounted) + + +/mob/living/simple_animal/hostile/retaliate/honse/get_sound(input) + switch(input) + if("aggro") + return pick('sound/vo/mobs/saiga/attack (1).ogg','sound/vo/mobs/saiga/attack (2).ogg') + if("pain") + return pick('sound/vo/mobs/saiga/pain (1).ogg','sound/vo/mobs/saiga/pain (2).ogg','sound/vo/mobs/saiga/pain (3).ogg') + if("death") + return pick('sound/vo/mobs/saiga/death (1).ogg','sound/vo/mobs/saiga/death (2).ogg') + if("idle") + return pick('sound/vo/mobs/saiga/idle (1).ogg','sound/vo/mobs/saiga/idle (2).ogg','sound/vo/mobs/saiga/idle (3).ogg','sound/vo/mobs/saiga/idle (4).ogg','sound/vo/mobs/saiga/idle (5).ogg','sound/vo/mobs/saiga/idle (6).ogg','sound/vo/mobs/saiga/idle (7).ogg') + + +/mob/living/simple_animal/hostile/retaliate/honse/tamed() + ..() + deaggroprob = 20 + if(can_buckle) + AddComponent(/datum/component/riding/saiga) + +/mob/living/simple_animal/hostile/retaliate/honse/death() + unbuckle_all_mobs() + return ..() + +/mob/living/simple_animal/hostile/retaliate/honse/simple_limb_hit(zone) + if(!zone) + return "" + switch(zone) + if(BODY_ZONE_PRECISE_R_EYE) + return "head" + if(BODY_ZONE_PRECISE_L_EYE) + return "head" + if(BODY_ZONE_PRECISE_NOSE) + return "snout" + if(BODY_ZONE_PRECISE_MOUTH) + return "snout" + if(BODY_ZONE_PRECISE_SKULL) + return "head" + if(BODY_ZONE_PRECISE_EARS) + return "head" + if(BODY_ZONE_PRECISE_NECK) + return "neck" + if(BODY_ZONE_PRECISE_L_HAND) + return "foreleg" + if(BODY_ZONE_PRECISE_R_HAND) + return "foreleg" + if(BODY_ZONE_PRECISE_L_FOOT) + return "leg" + if(BODY_ZONE_PRECISE_R_FOOT) + return "leg" + if(BODY_ZONE_PRECISE_STOMACH) + return "stomach" + if(BODY_ZONE_HEAD) + return "head" + if(BODY_ZONE_R_LEG) + return "leg" + if(BODY_ZONE_L_LEG) + return "leg" + if(BODY_ZONE_R_ARM) + return "foreleg" + if(BODY_ZONE_L_ARM) + return "foreleg" + return ..() + +/// If we're a mount and are hit while sprinting, throw our rider off +/// Also called if the rider is hit +/mob/living/simple_animal/hostile/retaliate/honse/proc/check_sprint_dismount() + SIGNAL_HANDLER + for(var/mob/living/carbon/human/rider in buckled_mobs) + if(rider.m_intent == MOVE_INTENT_RUN) + var/rider_skill = rider.get_skill_level(/datum/skill/misc/riding) + if(rider_skill < SKILL_LEVEL_MASTER) + violent_dismount(rider) + +/mob/living/simple_animal/hostile/retaliate/honse/post_buckle_mob(mob/living/M) + . = ..() + RegisterSignal(M, COMSIG_MOB_APPLY_DAMGE, PROC_REF(check_sprint_dismount)) + if(!has_buckled_mobs()) + RegisterSignal(src, COMSIG_MOB_APPLY_DAMGE, PROC_REF(check_sprint_dismount)) + +/mob/living/simple_animal/hostile/retaliate/honse/post_unbuckle_mob(mob/living/M) + . = ..() + UnregisterSignal(M, COMSIG_MOB_APPLY_DAMGE, PROC_REF(check_sprint_dismount)) + if(!has_buckled_mobs()) + UnregisterSignal(src, COMSIG_MOB_APPLY_DAMGE, PROC_REF(check_sprint_dismount)) + +/obj/effect/decal/remains/honse + name = "remains" + desc = "The remains of a once-proud honse. Perhaps it was killed for food, or slain in battle with a valiant knight atop?" + gender = PLURAL + icon_state = "skele" + icon = 'icons/mob/monster/fogbeast.dmi' + +/mob/living/simple_animal/hostile/retaliate/honse/male + name = "honse stallion" + gender = MALE + +/mob/living/simple_animal/hostile/retaliate/honse/male/tame + tame = TRUE + +/mob/living/simple_animal/hostile/retaliate/honse/male/tame/saddled/Initialize() + . = ..() + var/obj/item/natural/saddle/S = new(src) + ssaddle = S + update_icon() + +// FOAL +/mob/living/simple_animal/hostile/retaliate/honse/kid + name = "honse filly" + desc = "A young honse, likely to be running around with its mother. Honses are a distant cousin to the saiga, hailing from the mysterious islands of Kaizoku - rarer, but more strongly valued. Extensively used in the Steppes of Aavnr as pack animals and combat mounts." + icon = 'icons/mob/monster/fogbeast.dmi' + icon_state = "foggie" + icon_living = "foggie" + icon_dead = "foggie_dead" + icon_gib = "foggie_dead" + animal_species = null + emote_see = list("looks around.", "chews some leaves.", "neighs", "hops about playfully") + animal_species = null + butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/steak = 1, /obj/item/alch/bone = 3) + health = 20 + maxHealth = 20 + pass_flags = PASSTABLE | PASSMOB + mob_size = MOB_SIZE_SMALL + melee_damage_lower = 1 + melee_damage_upper = 6 + base_constitution = 5 + base_strength = 5 + base_speed = 5 + adult_growth = /mob/living/simple_animal/hostile/retaliate/honse + tame = TRUE + can_buckle = FALSE + can_saddle = FALSE + aggressive = TRUE + ai_controller = /datum/ai_controller/saiga_kid + can_breed = FALSE + generate_genetics = FALSE + +/mob/living/simple_animal/hostile/retaliate/honse/kid/male + name = "honse colt" + adult_growth = /mob/living/simple_animal/hostile/retaliate/honse/male + +// INTENT +/datum/intent/simple/honse + name = "horse" + icon_state = "instrike" + attack_verb = list("tramples", "rams", "kicks") + animname = "blank22" + blade_class = BCLASS_BLUNT + hitsound = "punch_hard" + chargetime = 0 + penfactor = 10 + swingdelay = 0 + candodge = TRUE + canparry = TRUE + item_damage_type = "blunt" + clickcd = CLICK_CD_MELEE * 1.1 diff --git a/code/modules/mob/living/simple_animal/hostile/retaliate/game/saiga.dm b/code/modules/mob/living/simple_animal/hostile/retaliate/game/saiga.dm index ca9644253e2..eea0697d140 100644 --- a/code/modules/mob/living/simple_animal/hostile/retaliate/game/saiga.dm +++ b/code/modules/mob/living/simple_animal/hostile/retaliate/game/saiga.dm @@ -59,6 +59,9 @@ ai_controller = /datum/ai_controller/saiga + genetics = /datum/animal_genetics/saiga + generate_genetics = TRUE + var/can_breed = TRUE var/static/list/pet_commands = list( @@ -89,6 +92,26 @@ /mob/living/simple_animal/hostile/retaliate/saiga/update_overlays() . = ..() + + if(istype(genetics)) + var/datum/animal_gene/undercoat/UC = genetics.get_gene_by_exclusion_group(GENE_GROUP_UNDERCOAT) + var/datum/animal_gene/coat_color/CC = genetics.get_gene_by_exclusion_group(GENE_GROUP_COAT_COLOR) + var/datum/animal_gene/emissive = genetics.get_gene_by_exclusion_group(GENE_GROUP_EMISSIVE) + + var/mutable_appearance/body = mutable_appearance(icon, "[icon_state]_reg1") + var/mutable_appearance/underbody = mutable_appearance(icon, "[icon_state]_reg2") + if(emissive) + var/mutable_appearance/glowing = emissive_appearance(icon, "[icon_state]_reg2") + . += glowing + if(CC) + body.color = CC.get_color() + if(!UC) + underbody.color = CC.get_color() + else + underbody.color = UC.get_color() + . += body + . += underbody + if(stat <= DEAD) return if(ssaddle) @@ -114,8 +137,6 @@ CALLBACK(src, PROC_REF(after_birth)),\ ) -/mob/living/simple_animal/hostile/retaliate/saiga/proc/after_birth(mob/living/simple_animal/hostile/retaliate/cow/cowlet/baby, mob/living/partner) - return /mob/living/simple_animal/hostile/retaliate/saiga/get_sound(input) switch(input) @@ -198,6 +219,9 @@ ai_controller = /datum/ai_controller/saiga + genetics = /datum/animal_genetics/saiga + generate_genetics = TRUE + var/static/list/pet_commands = list( /datum/pet_command/idle, /datum/pet_command/free, @@ -226,6 +250,26 @@ /mob/living/simple_animal/hostile/retaliate/saigabuck/update_overlays() . = ..() + + if(istype(genetics)) + var/datum/animal_gene/undercoat/UC = genetics.get_gene_by_exclusion_group(GENE_GROUP_UNDERCOAT) + var/datum/animal_gene/coat_color/CC = genetics.get_gene_by_exclusion_group(GENE_GROUP_COAT_COLOR) + var/datum/animal_gene/emissive = genetics.get_gene_by_exclusion_group(GENE_GROUP_EMISSIVE) + + var/mutable_appearance/body = mutable_appearance(icon, "[icon_state]_reg1") + var/mutable_appearance/underbody = mutable_appearance(icon, "[icon_state]_reg2") + if(emissive) + var/mutable_appearance/glowing = emissive_appearance(icon, "[icon_state]_reg2") + . += glowing + if(CC) + body.color = CC.get_color() + if(!UC) + underbody.color = CC.get_color() + else + underbody.color = UC.get_color() + . += body + . += underbody + if(stat <= DEAD) return if(ssaddle) @@ -301,6 +345,8 @@ can_breed = FALSE + generate_genetics = FALSE + ai_controller = /datum/ai_controller/saiga_kid /mob/living/simple_animal/hostile/retaliate/saiga/saigakid/boy diff --git a/code/modules/mob/living/simple_animal/hostile/retaliate/mirespider.dm b/code/modules/mob/living/simple_animal/hostile/retaliate/mirespider.dm new file mode 100644 index 00000000000..e2c8f16d6b3 --- /dev/null +++ b/code/modules/mob/living/simple_animal/hostile/retaliate/mirespider.dm @@ -0,0 +1,393 @@ +/mob/living/simple_animal/hostile/retaliate/mirespider + icon = 'icons/mob/mirespider_small.dmi' + desc = "Said to have originated from the decapitated heads of fallen legionnaires from eons past, grown legs and a voracious appetite, mire crawlers are common pests in many a wetland. Occasionally hunted for their silk." + name = "mire crawler" + icon_state = "crawler" + icon_living = "crawler" + icon_dead = "crawler_dead" + mob_biotypes = MOB_ORGANIC|MOB_BEAST + emote_hear = null + emote_see = null + speak_chance = 1 + see_in_dark = 10 + move_to_delay = 3 + + faction = list("zombie", "spiders") + attack_sound = list('sound/vo/mobs/spider/attack (1).ogg','sound/vo/mobs/spider/attack (2).ogg','sound/vo/mobs/spider/attack (3).ogg','sound/vo/mobs/spider/attack (4).ogg') + + base_intents = list(/datum/intent/simple/bite/mirespider) + botched_butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/steak = 1) + butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/steak = 1, + /obj/item/natural/hide = 1, + /obj/item/natural/silk = 1, + /obj/item/alch/viscera = 1) + perfect_butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/steak = 1, + /obj/item/natural/hide = 1, + /obj/item/natural/silk = 2, + /obj/item/alch/viscera = 1) + + health = 80 + maxHealth = 80 + melee_damage_lower = 17 + melee_damage_upper = 26 + vision_range = 10 + aggro_vision_range = 9 + environment_smash = ENVIRONMENT_SMASH_NONE + retreat_distance = 0 + minimum_distance = 0 + + base_constitution = 7 + base_strength = 7 + base_speed = 13 + footstep_type = FOOTSTEP_MOB_BAREFOOT + defprob = 40 + retreat_health = 0 + ai_controller = /datum/ai_controller/mirespider + +/mob/living/simple_animal/hostile/retaliate/mirespider/Initialize() + . = ..() + update_icon() + AddElement(/datum/element/ai_retaliate) + AddComponent(/datum/component/ai_aggro_system) + ADD_TRAIT(src, TRAIT_NOPAINSTUN, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_KNEESTINGER_IMMUNITY, INNATE_TRAIT) + + addtimer(CALLBACK(src, PROC_REF(find_lurker_to_follow)), 10) + +/mob/living/simple_animal/hostile/retaliate/mirespider/proc/find_lurker_to_follow() + var/mob/living/simple_animal/hostile/mirespider_lurker/lurker = null + for(var/mob/living/simple_animal/hostile/mirespider_lurker/L in view(10, src)) + { + lurker = L + break + } + + if(lurker && ai_controller) + ai_controller.set_blackboard_key(BB_FOLLOW_TARGET, lurker) + +/mob/living/simple_animal/hostile/retaliate/mirespider/death(gibbed) + ..() + update_icon() + +/datum/intent/simple/bite/mirespider + clickcd = CLICK_CD_MELEE * 1.1 + +/mob/living/simple_animal/hostile/retaliate/mirespider/get_sound(input) + switch(input) + if("aggro") + return pick('sound/vo/mobs/spider/aggro (1).ogg','sound/vo/mobs/spider/aggro (2).ogg','sound/vo/mobs/spider/aggro (3).ogg') + if("pain") + return pick('sound/vo/mobs/spider/pain.ogg') + if("death") + return pick('sound/vo/mobs/spider/death.ogg') + if("idle") + return pick('sound/vo/mobs/spider/idle (1).ogg','sound/vo/mobs/spider/idle (2).ogg','sound/vo/mobs/spider/idle (3).ogg','sound/vo/mobs/spider/idle (4).ogg') + +/mob/living/simple_animal/hostile/retaliate/mirespider/taunted(mob/user) + emote("aggro") + return + +/mob/living/simple_animal/hostile/retaliate/mirespider/simple_limb_hit(zone) + if(!zone) + return "" + switch(zone) + if(BODY_ZONE_PRECISE_R_EYE) + return "head" + if(BODY_ZONE_PRECISE_L_EYE) + return "head" + if(BODY_ZONE_PRECISE_NOSE) + return "nose" + if(BODY_ZONE_PRECISE_MOUTH) + return "mouth" + if(BODY_ZONE_PRECISE_SKULL) + return "head" + if(BODY_ZONE_PRECISE_EARS) + return "head" + if(BODY_ZONE_PRECISE_NECK) + return "neck" + if(BODY_ZONE_PRECISE_L_HAND) + return "foreleg" + if(BODY_ZONE_PRECISE_R_HAND) + return "foreleg" + if(BODY_ZONE_PRECISE_L_FOOT) + return "leg" + if(BODY_ZONE_PRECISE_R_FOOT) + return "leg" + if(BODY_ZONE_PRECISE_STOMACH) + return "stomach" + if(BODY_ZONE_PRECISE_GROIN) + return "tail" + if(BODY_ZONE_HEAD) + return "head" + if(BODY_ZONE_R_LEG) + return "leg" + if(BODY_ZONE_L_LEG) + return "leg" + if(BODY_ZONE_R_ARM) + return "foreleg" + if(BODY_ZONE_L_ARM) + return "foreleg" + return ..() + +/mob/living/simple_animal/hostile/mirespider_lurker + icon = 'icons/mob/mirespider_big.dmi' + desc = "An unusually large and dangerous mire crawler, these lumbering creatures tend to find smaller specimens gravitating to them for safety - or perhaps simply to hunt more efficiently." + name = "mire lurker" + icon_state = "lurker" + icon_living = "lurker" + icon_dead = "lurker_dead" + + faction = list("zombie", "spiders") + attack_sound = list('sound/vo/mobs/spider/attack (1).ogg','sound/vo/mobs/spider/attack (2).ogg','sound/vo/mobs/spider/attack (3).ogg','sound/vo/mobs/spider/attack (4).ogg') + + base_intents = list(/datum/intent/simple/bite/mirespider_lurker) + botched_butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/steak = 2, + /obj/item/natural/hide = 2, + /obj/item/natural/silk = 1, + /obj/item/alch/viscera = 1) + butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/steak = 4, + /obj/item/natural/hide = 3, + /obj/item/natural/silk = 3, + /obj/item/alch/viscera = 4) + perfect_butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/steak = 4, + /obj/item/natural/hide = 4, + /obj/item/natural/silk = 5, // You killed the mire lurker. You get all the figgy pudding . . . + /obj/item/alch/viscera = 4) + + health = 200 + maxHealth = 200 + melee_damage_lower = 35 + melee_damage_upper = 70 + + base_constitution = 9 + base_strength = 9 + base_speed = 14 + // These things will crit. Slow attacks, devestating consequences. + base_perception = 15 + pixel_x = -4 + + ai_controller = /datum/ai_controller/mirespider_lurker + projectiletype = /obj/projectile/bullet/spider + + ranged = 1 + minimum_distance = 1 + ranged_cooldown_time = 100 + var/list/mob/living/simple_animal/hostile/retaliate/mirespider/followers = list() + +/mob/living/simple_animal/hostile/mirespider_lurker/mushroom + icon = 'icons/mob/mirespider_shroom.dmi' + desc = "While recognizable as a mire lurker, this specimen appears to suffer a gigantic \ + fungal growth over its rear end. It reeks of the smell of mold, and tar-like secretions \ + drip from its mandibles. Something here is horribly wrong." + name = "mire lurker?" + icon_state = "mushroom" + icon_living = "mushroom" + icon_dead = "mushroom_dead" + health = 400 + maxHealth = 400 + pixel_x = -8 + + projectiletype = /obj/projectile/bullet/spider_shroom + botched_butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/steak = 2, + /obj/item/natural/hide = 2, + /obj/item/natural/silk = 1, + /obj/item/reagent_containers/powder/ozium = 1, + /obj/item/alch/viscera = 1) + butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/steak = 4, + /obj/item/natural/hide = 3, + /obj/item/natural/silk = 3, + /obj/item/reagent_containers/powder/ozium = 2, + /obj/item/alch/viscera = 4) + perfect_butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/steak = 4, + /obj/item/natural/hide = 4, + /obj/item/natural/silk = 5, // You killed the mire lurker. You get all the figgy pudding . . . + /obj/item/reagent_containers/powder/ozium = 2, + /obj/item/reagent_containers/powder/moondust = 1, + /obj/item/alch/viscera = 4) + +/mob/living/simple_animal/hostile/mirespider_lurker/Initialize() + . = ..() + ADD_TRAIT(src, TRAIT_NOPAINSTUN, TRAIT_GENERIC) + ADD_TRAIT(src, TRAIT_KNEESTINGER_IMMUNITY, INNATE_TRAIT) + AddComponent(/datum/component/ai_aggro_system) + // I'll replace this with something better later. Stopgap for now to make killing them more than just a nuisance. + +/mob/living/simple_animal/hostile/mirespider_lurker/death(gibbed) + ..() + if(prob(40)) + new /obj/item/reagent_containers/food/snacks/spiderhoney(loc) + if(prob(10)) + new /obj/item/gem/violet(loc) + update_icon() + +/datum/intent/simple/bite/mirespider_lurker + clickcd = CLICK_CD_MELEE * 1.1 + +/mob/living/simple_animal/hostile/mirespider_lurker/proc/add_follower(mob/living/simple_animal/hostile/retaliate/mirespider) + if (!(mirespider in followers)) + followers += mirespider + +/mob/living/simple_animal/hostile/mirespider_lurker/proc/clear_followers_if_any() + if (!followers || !length(followers)) + return + + for (var/mob/living/simple_animal/hostile/retaliate/mirespider/follower in followers) + follower.ai_controller.clear_blackboard_key(BB_FOLLOW_TARGET) + follower.ai_controller.clear_blackboard_key(BB_TRAVEL_DESTINATION) + follower.ai_controller.clear_blackboard_key(BB_BASIC_MOB_CURRENT_TARGET) + follower.ai_controller.clear_blackboard_key(BB_BASIC_MOB_RETALIATE_LIST) + follower.ai_controller.CancelActions() + followers.Cut() + +/mob/living/simple_animal/hostile/mirespider_paralytic + icon = 'icons/mob/mirespider_small.dmi' + name = "aragn" + desc = "A gigantic species of spider accompanied always by a strong sulphuric stench. Its fangs carry \ + a dangerous paralytic; a danger for the common traveller, and an opportunity to any aspiring poisoner." + icon_state = "aragn" + icon_living = "aragn" + icon_dead = "aragn_dead" + + faction = list("zombie", "spiders") + attack_sound = list('sound/vo/mobs/spider/attack (1).ogg','sound/vo/mobs/spider/attack (2).ogg','sound/vo/mobs/spider/attack (3).ogg','sound/vo/mobs/spider/attack (4).ogg') + + base_intents = list(/datum/intent/simple/bite/mirespider_paralytic) + botched_butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/steak = 1, + /obj/item/natural/silk = 1, + /obj/item/alch/viscera = 1) + butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/steak = 2, + /obj/item/natural/hide = 1, + /obj/item/natural/silk = 1, + /obj/item/alch/viscera = 1, + /obj/item/reagent_containers/spidervenom_inert = 1) + perfect_butcher_results = list(/obj/item/reagent_containers/food/snacks/meat/steak = 2, + /obj/item/natural/hide = 1, + /obj/item/natural/silk = 1, + /obj/item/alch/viscera = 1, + /obj/item/reagent_containers/spidervenom_inert = 2) + + health = 140 + maxHealth = 140 + melee_damage_lower = 21 + melee_damage_upper = 42 + + base_constitution = 9 + base_strength = 9 + base_speed = 12 + base_perception = 7 + + ai_controller = /datum/ai_controller/mirespider_paralytic + +/mob/living/simple_animal/hostile/mirespider_paralytic/Initialize() + . = ..() + AddComponent(/datum/component/ai_aggro_system) + +/datum/intent/simple/bite/mirespider_paralytic + clickcd = CLICK_CD_MELEE * 1.1 + +/mob/living/simple_animal/hostile/mirespider_paralytic/AttackingTarget() + . = ..() + if(. && isliving(target)) + var/mob/living/L = target + if(L.reagents && prob(50)) + L.reagents.add_reagent(/datum/reagent/toxin/spidervenom_paralytic, 5) + +/obj/random/spider + icon = 'icons/mob/mirespider_small.dmi' + name = "random spider spawner" + desc = "YOU SHOULD NOT BE SEEING THIS, GO YELL AT KETRAI." + icon_state = "crawler" + +/obj/random/spider/Initialize() + . = ..() + spawn_random_spider_at(loc) + qdel(src) + +/obj/random/spider/proc/spawn_random_spider_at(turf/T) + var/newspider = list(/mob/living/simple_animal/hostile/mirespider_paralytic = 10, /mob/living/simple_animal/hostile/retaliate/mirespider = 90) + var/spider_to_spawn = pickweight(newspider) + new spider_to_spawn(get_turf(src)) + qdel(src) + +/mob/living/simple_animal/hostile/retaliate/mirespider/angry + faction = list("mad", "zombie") + +/mob/living/simple_animal/hostile/mirespider_paralytic/angry + faction = list("mad", "zombie") + +/mob/living/simple_animal/hostile/mirespider_lurker/angry + faction = list("mad", "zombie") + +/obj/projectile/bullet/spider + name = "web glob" + damage = 10 + damage_type = BRUTE + icon = 'icons/obj/webbing.dmi' + icon_state = "webglob" + range = 15 + hitsound = 'sound/combat/hits/hi_arrow2.ogg' + embedchance = 0 + //Will not cause wounds. + woundclass = null + flag = "piercing" + speed = 2 // I guess slower to be slightly more forgiving to players since they're otherwise aimbots + +/obj/projectile/bullet/spider_shroom + name = "web glob" + damage = 10 + damage_type = BRUTE + icon = 'icons/obj/webbing.dmi' + icon_state = "webglob" + range = 15 + hitsound = 'sound/combat/hits/hi_arrow2.ogg' + embedchance = 0 + //Will not cause wounds. + woundclass = null + flag = "piercing" + speed = 2 + +/obj/projectile/bullet/spider/on_hit(target) + . = ..() + if(ismob(target)) + var/mob/living/M = target + //M.apply_status_effect(/datum/status_effect/debuff/vulnerable) + M.Immobilize(15) + var/turf/T + if(isturf(target)) + T = target + else + T = get_turf(target) + var/web = locate(/obj/structure/spider/stickyweb/mirespider) in T.contents + if(!(web in T.contents)) + new /obj/structure/spider/stickyweb/mirespider(T) + +/obj/projectile/bullet/spider_shroom/on_hit(target) + . = ..() + if(ismob(target)) + var/mob/living/M = target + //M.apply_status_effect(/datum/status_effect/debuff/vulnerable) + M.apply_status_effect(/datum/status_effect/buff/druqks) + var/turf/T + if(isturf(target)) + T = target + else + T = get_turf(target) + var/web = locate(/obj/structure/spider/stickyweb/mirespider) in T.contents + if(!(web in T.contents)) + new /obj/structure/spider/stickyweb/mirespider(T) + +//Mirespider webs are thinner and will not stop projectiles or obstruct movement as often. +/obj/structure/spider/stickyweb/mirespider + opacity = 0 + pass_flags = LETPASSTHROW + debris = null + +/obj/structure/spider/stickyweb/mirespider/CanPass(atom/movable/mover, turf/target) + . = ..() + if(isliving(mover)) + if(prob(25) && !HAS_TRAIT(mover, TRAIT_WEBWALK)) + to_chat(mover, "I get stuck in \the [src] for a moment.") + return FALSE + else if(istype(mover, /obj/projectile)) + return prob(85) + return TRUE diff --git a/code/modules/mob/living/simple_animal/hostile/retaliate/retaliate.dm b/code/modules/mob/living/simple_animal/hostile/retaliate/retaliate.dm index a1d0b638259..a6f3657c2bb 100644 --- a/code/modules/mob/living/simple_animal/hostile/retaliate/retaliate.dm +++ b/code/modules/mob/living/simple_animal/hostile/retaliate/retaliate.dm @@ -157,6 +157,8 @@ var/old_hunger_percentage = old_hunger.current_hunger / old_hunger.max_hunger hunger.current_hunger = hunger.max_hunger * old_hunger_percentage + if(istype(genetics)) + genetics?.copy_to(A) qdel(src) return diff --git a/code/modules/mob/living/simple_animal/simple_animal.dm b/code/modules/mob/living/simple_animal/simple_animal.dm index 74c63d54db0..137ac19166d 100644 --- a/code/modules/mob/living/simple_animal/simple_animal.dm +++ b/code/modules/mob/living/simple_animal/simple_animal.dm @@ -171,6 +171,15 @@ GLOBAL_VAR_INIT(farm_animals, FALSE) "best_friend" = 100 ) + var/obj/item/caparison/ccaparison + var/obj/item/clothing/barding/bbarding + var/caparison_over_barding = FALSE + + var/datum/animal_genetics/genetics = /datum/animal_genetics + var/generate_genetics = FALSE + var/genetic_butcher_scale = 1.0 + var/genetic_speed_delta = 0 + /mob/living/simple_animal/Initialize() . = ..() if(gender == PLURAL) @@ -192,6 +201,10 @@ GLOBAL_VAR_INIT(farm_animals, FALSE) if(happy_funtime_mob) AddComponent(/datum/component/friendship_container, mob_friends, "friend") AddComponent(/datum/component/happiness_container, 30, list(), list(), food_type) + if(generate_genetics) + genetics = new genetics(src) + genetics.roll_guaranteed_genes() + roll_initial_genetics() /mob/living/simple_animal/Destroy() if(nest) @@ -201,8 +214,75 @@ GLOBAL_VAR_INIT(farm_animals, FALSE) if(ssaddle) QDEL_NULL(ssaddle) + if(ccaparison) + QDEL_NULL(ccaparison) + ccaparison = null + + if(!ispath(genetics)) + QDEL_NULL(genetics) + return ..() + + +/mob/living/simple_animal/attack_hand_secondary(mob/user, list/modifiers) + . = ..() + if(ccaparison) + user.visible_message(span_notice("[user] is removing the caparison from [src]..."), span_notice("I start removing the caparison from [src]...")) + if(!do_after(user, 10 SECONDS, src)) + return + playsound(loc, 'sound/foley/saddledismount.ogg', 100, FALSE) + user.visible_message(span_notice("[user] removes the caparison from [src]."), span_notice("I remove the caparison from [src].")) + var/obj/item/caparison/C = ccaparison + ccaparison = null + C.forceMove(get_turf(src)) + user.put_in_hands(C) + update_appearance() + return + else if(bbarding) + user.visible_message(span_notice("[user] is removing the bard from [src]..."), span_notice("I start removing the bard from [src]...")) + if(!do_after(user, 10 SECONDS, src)) + return + playsound(loc, 'sound/foley/saddledismount.ogg', 100, FALSE) + user.visible_message(span_notice("[user] removes the bard from [src]."), span_notice("I remove the bard from [src].")) + var/obj/item/clothing/barding/B = bbarding + bbarding = null + B.forceMove(get_turf(src)) + user.put_in_hands(B) + update_appearance() + return + else if(ssaddle) + user.visible_message(span_notice("[user] is removing the saddle from [src]..."), span_notice("I start removing the saddle from [src]...")) + if(!do_after(user, 5 SECONDS, src)) + return + playsound(loc, 'sound/foley/saddledismount.ogg', 100, FALSE) + user.visible_message(span_notice("[user] removes the saddle from [src]."), span_notice("I remove the saddle from [src].")) + var/obj/item/natural/saddle/S = ssaddle + ssaddle = null + S.forceMove(get_turf(src)) + user.put_in_hands(S) + update_appearance() + return return ..() +/mob/living/simple_animal/update_overlays() + . = ..() + var/barding_layer = 6 + var/caparison_layer = 5 + if(caparison_over_barding) + caparison_layer = 6 + barding_layer = 5 + if(ccaparison && stat == CONSCIOUS && !resting) + var/caparison_overlay = ccaparison.female_caparison_state && gender == FEMALE ? ccaparison.female_caparison_state : ccaparison.caparison_state + var/mutable_appearance/caparison_base_overlay = mutable_appearance(ccaparison.caparison_icon, caparison_overlay, caparison_layer) + var/mutable_appearance/caparison_above_overlay = mutable_appearance(ccaparison.caparison_icon, caparison_overlay + "-above", caparison_layer - 0.69) + . += caparison_base_overlay + . += caparison_above_overlay + if(bbarding && stat == CONSCIOUS && !resting) + var/barding_overlay = bbarding.female_barding_state && gender == FEMALE ? bbarding.female_barding_state : bbarding.barding_state + var/mutable_appearance/barding_base_overlay = mutable_appearance(bbarding.barding_icon, barding_overlay, barding_layer) + var/mutable_appearance/barding_above_overlay = mutable_appearance(bbarding.barding_icon, barding_overlay + "-above", barding_layer - 0.69) + . += barding_base_overlay + . += barding_above_overlay + /mob/living/simple_animal/attackby(obj/item/O, mob/user, list/modifiers) if(!is_type_in_list(O, food_type)) return ..() @@ -266,6 +346,7 @@ GLOBAL_VAR_INIT(farm_animals, FALSE) if(user) owner = user + update_appearance() //mob/living/simple_animal/examine(mob/user) // . = ..() @@ -329,6 +410,11 @@ GLOBAL_VAR_INIT(farm_animals, FALSE) if(stuttering) stuttering = 0 +/mob/living/simple_animal/proc/after_birth(mob/living/simple_animal/baby, mob/living/partner) + if(genetics && !ispath(genetics)) + genetics.inherit_to(baby, partner) + return + /mob/living/simple_animal/proc/handle_automated_speech(override) set waitfor = FALSE if(speak_chance) @@ -391,6 +477,25 @@ GLOBAL_VAR_INIT(farm_animals, FALSE) playsound(src, 'sound/foley/gross.ogg', 100, FALSE) if(do_after(user, 3 SECONDS, src)) butcher(user) + else if (stat != DEAD && istype(ssaddle, /obj/item/natural/saddle) && bbarding && ccaparison) + var/pick = browser_alert(user, "What would you like to do?", "[src.name]", list("Adjust caparison", "Look through the saddle bags")) + if(!pick) + pick = "Look through the saddle bags" + switch(pick) + if("Adjust caparison") + caparison_over_barding = !caparison_over_barding + to_chat(user, span_info("I [caparison_over_barding ? "adjust [ccaparison] to cover [bbarding]" : "adjust [ccaparison] to be under [bbarding]"].")) + update_appearance() + if("Look through the saddle bags") + var/datum/component/storage/saddle_storage = ssaddle.GetComponent(/datum/component/storage) + var/access_time = (user in buckled_mobs) ? 10 : 30 + if (do_after(user, access_time, target = src)) + saddle_storage.show_to(user) + else if(bbarding && ccaparison) + caparison_over_barding = !caparison_over_barding + to_chat(user, span_info("I [caparison_over_barding ? "adjust [ccaparison] to cover [bbarding]" : "adjust [ccaparison] to be under [bbarding]"].")) + update_appearance() + ..() /mob/living/simple_animal/proc/butcher(mob/living/user) @@ -434,7 +539,7 @@ GLOBAL_VAR_INIT(farm_animals, FALSE) var/bonus_count = 0 // Track bonus items from happiness for(var/path in butcher_results) - var/amount = butcher_results[path] + var/amount = round(butcher_results[path] * genetic_butcher_scale) if(!do_after(user, time_per_cut, target = src)) if(botch_count || normal_count || perfect_count || bonus_count) to_chat(user, span_notice("I stop butchering: [butcher_summary(botch_count, normal_count, perfect_count, bonus_count, botch_chance, perfect_chance, happiness_bonus)].")) @@ -445,12 +550,12 @@ GLOBAL_VAR_INIT(farm_animals, FALSE) if(prob(botch_chance)) botch_count++ if(length(botched_butcher_results) && (path in botched_butcher_results)) - amount = botched_butcher_results[path] + amount = round(botched_butcher_results[path] * genetic_butcher_scale, 1) else amount = 0 // Otherwise check for perfect else if(length(perfect_butcher_results) && (path in perfect_butcher_results) && prob(perfect_chance)) - amount = perfect_butcher_results[path] + amount = round(perfect_butcher_results[path] * genetic_butcher_scale, 1) perfect_count++ else normal_count++ @@ -887,6 +992,21 @@ GLOBAL_VAR_INIT(farm_animals, FALSE) playsound(L, 'sound/foley/zfall.ogg', 100, FALSE) L.visible_message(span_danger("[L] falls off [src]!")) +/mob/living/simple_animal/proc/violent_dismount(mob/living/user) + if(isliving(user)) + var/mob/living/L = user + unbuckle_mob(L) + L.Paralyze(5 SECONDS) + L.Stun(5 SECONDS) + playsound(L.loc, 'sound/foley/zfall.ogg', 100, FALSE) + L.visible_message(span_danger("[L] falls off [src]!")) + +/mob/living/simple_animal/proc/apply_gene(datum/animal_gene/G) + G.apply_to(src) + +/mob/living/simple_animal/proc/remove_gene(datum/animal_gene/G) + G.remove_from(src) + /mob/living/simple_animal/buckle_mob(mob/living/buckled_mob, force = 0, check_loc = 1) . = ..() LoadComponent(/datum/component/riding) diff --git a/code/modules/mob/mob_helpers.dm b/code/modules/mob/mob_helpers.dm index ffb2d942a1e..c37d81c3a95 100644 --- a/code/modules/mob/mob_helpers.dm +++ b/code/modules/mob/mob_helpers.dm @@ -995,7 +995,9 @@ used_title = return_our_apprentice_name() else if(job) var/datum/job/job_datum = SSjob.GetJob(job) - var/datum/job/used_job = job_datum.parent_job ? job_datum.parent_job : job_datum + if(!job_datum) + return job + var/datum/job/used_job = job_datum?.parent_job ? job_datum.parent_job : job_datum if(!used_job) return job if(steward_check && (used_job.department_flag == OUTSIDERS)) diff --git a/code/modules/projectiles/guns/ballistic/launchers.dm b/code/modules/projectiles/guns/ballistic/launchers.dm index 0c1d0cf57a2..867df88826f 100644 --- a/code/modules/projectiles/guns/ballistic/launchers.dm +++ b/code/modules/projectiles/guns/ballistic/launchers.dm @@ -10,6 +10,7 @@ w_class = WEIGHT_CLASS_NORMAL bolt_type = BOLT_TYPE_NO_BOLT istrainable = TRUE // For the moment I'll allow these to be traineable until a proper way to level up bows and crossbows is coded. - Foxtrot + flags_ai_inventory = AI_ITEM_GUN var/damfactor = 1 // Multiplier for projectile damage. Used by bows and crossbows. /obj/item/gun/ballistic/revolver/grenadelauncher/attackby(obj/item/A, mob/user, list/modifiers) diff --git a/code/modules/projectiles/projectile.dm b/code/modules/projectiles/projectile.dm index 4680d4edd80..6c9a5983c2f 100644 --- a/code/modules/projectiles/projectile.dm +++ b/code/modules/projectiles/projectile.dm @@ -812,7 +812,7 @@ xo = targloc.x - curloc.x setAngle(get_angle(src, targloc) + spread) - if(isliving(source) && modifiers) + if(isliving(source) && length(modifiers)) var/list/calculated = calculate_projectile_angle_and_pixel_offsets(source, modifiers) p_x = calculated[2] p_y = calculated[3] diff --git a/code/modules/questing/contract_machine.dm b/code/modules/questing/contract_machine.dm new file mode 100644 index 00000000000..b51266d26be --- /dev/null +++ b/code/modules/questing/contract_machine.dm @@ -0,0 +1,376 @@ +/obj/structure/fake_machine/contractledger + name = "Grand Contract Ledger" + desc = "A massive ledger book with gilded edges, sitting atop a pedestal with the Mercenary's Guild banner. Its myriad enchanted pages are filled with various contracts and bounties issued by Mercenary's Guild, with arcane scripts that appears and fades as contracts are issued and completed." + icon = 'icons/obj/questing.dmi' + icon_state = "contractledger" + density = TRUE + anchored = TRUE + max_integrity = 0 + layer = ABOVE_MOB_LAYER + layer = GAME_PLANE_UPPER + var/input_point + +/obj/structure/fake_machine/contractledger/Initialize() + . = ..() + input_point = locate(x, y - 1, z) + var/obj/effect/decal/marker_export/marker = new(get_turf(input_point)) + marker.desc = "Place completed contract scrolls here to turn them in." + marker.layer = ABOVE_OBJ_LAYER + +/obj/structure/fake_machine/contractledger/attackby(obj/item/P, mob/living/carbon/human/user, params) + . = .. () + if(istype(P, /obj/item/paper/scroll/quest)) + turn_in_contract(user, P) + return + return + +/obj/structure/fake_machine/contractledger/Topic(href, href_list) + . = ..() + if(href_list["consultcontracts"]) + consult_contracts(usr) + return attack_hand(usr) + if(href_list["turnincontract"]) + turn_in_contract(usr) + return attack_hand(usr) + if(href_list["abandoncontract"]) + abandon_contract(usr) + return attack_hand(usr) + if(href_list["printcontracts"]) + print_contracts(usr) + return attack_hand(usr) + return attack_hand(usr) + +/obj/structure/fake_machine/contractledger/attack_hand(mob/living/carbon/human/user) + if(!ishuman(user)) + return + // Inshallah I'll make this TGUI one day. + var/contents = "