From fe929e5849c62fba901269b160850d65a0da151f Mon Sep 17 00:00:00 2001 From: Rob Nelson Date: Sat, 25 Feb 2017 15:04:06 -0800 Subject: [PATCH] Component Model + PoC Mob (#13866) * Initial work on component mobs. * Revert simple_animal.dm * Fix movement. * Component signals are now #defines. * Fix magic number in atmos.dm * Added basic melee attacks. * Get rid of test spam --- code/__DEFINES/component_signals.dm | 72 ++++++++++++++ code/__HELPERS/cmp.dm | 7 ++ code/__HELPERS/maths.dm | 10 ++ code/modules/components/ai/ai_component.dm | 13 +++ code/modules/components/ai/atmos.dm | 97 ++++++++++++++++++ code/modules/components/ai/controller.dm | 64 ++++++++++++ .../ai/controllers/mob_controller.dm | 26 +++++ .../ai/controllers/simple_animal.dm | 8 ++ code/modules/components/ai/door_opener.dm | 78 +++++++++++++++ .../ai/hostile/escape_confinement.dm | 51 ++++++++++ code/modules/components/ai/hostile/hunt.dm | 53 ++++++++++ .../ai/hostile/melee/attack_animal.dm | 7 ++ .../ai/hostile/melee/inject_reagent.dm | 19 ++++ .../components/ai/hostile/melee/melee.dm | 14 +++ .../components/ai/mobs/component_mob.dm | 25 +++++ .../components/ai/mobs/giant_spider.dm | 22 +++++ .../ai/target_finders/simple_view.dm | 8 ++ .../ai/target_finders/target_finder.dm | 14 +++ .../ai/target_holders/prioritizing_holder.dm | 55 +++++++++++ .../ai/target_holders/target_holder.dm | 19 ++++ code/modules/components/component.dm | 24 +++++ .../modules/components/component_container.dm | 99 +++++++++++++++++++ code/modules/mob/living/living.dm | 8 +- vgstation13.dme | 20 ++++ 24 files changed, 810 insertions(+), 3 deletions(-) create mode 100644 code/__DEFINES/component_signals.dm create mode 100644 code/modules/components/ai/ai_component.dm create mode 100644 code/modules/components/ai/atmos.dm create mode 100644 code/modules/components/ai/controller.dm create mode 100644 code/modules/components/ai/controllers/mob_controller.dm create mode 100644 code/modules/components/ai/controllers/simple_animal.dm create mode 100644 code/modules/components/ai/door_opener.dm create mode 100644 code/modules/components/ai/hostile/escape_confinement.dm create mode 100644 code/modules/components/ai/hostile/hunt.dm create mode 100644 code/modules/components/ai/hostile/melee/attack_animal.dm create mode 100644 code/modules/components/ai/hostile/melee/inject_reagent.dm create mode 100644 code/modules/components/ai/hostile/melee/melee.dm create mode 100644 code/modules/components/ai/mobs/component_mob.dm create mode 100644 code/modules/components/ai/mobs/giant_spider.dm create mode 100644 code/modules/components/ai/target_finders/simple_view.dm create mode 100644 code/modules/components/ai/target_finders/target_finder.dm create mode 100644 code/modules/components/ai/target_holders/prioritizing_holder.dm create mode 100644 code/modules/components/ai/target_holders/target_holder.dm create mode 100644 code/modules/components/component.dm create mode 100644 code/modules/components/component_container.dm diff --git a/code/__DEFINES/component_signals.dm b/code/__DEFINES/component_signals.dm new file mode 100644 index 00000000000..6aed075864c --- /dev/null +++ b/code/__DEFINES/component_signals.dm @@ -0,0 +1,72 @@ +// Component Signal names. +// Avoids any mishaps caused by typos. + +/** Sent when a mob AI component wants to set new machine state. + * @param state mixed: The new machine state (HOSTILE_STANCE_IDLE, etc) + */ +#define COMSIG_STATE "state" + +/** Sent when we've been bumped. + * @param movable /atom/movable: The bumping entity. + */ +#define COMSIG_BUMPED "bumped" + +/** Sent when we've bumped someone else. + * @param movable /atom/movable: The bumped entity. + */ +#define COMSIG_BUMP "bump" + +/** Sent by mob Life() tick. No arguments. + */ +#define COMSIG_LIFE "life" + +/** Sent when a mob AI component has identified a new target. + * @param target /atom: The targetted entity. + */ +#define COMSIG_TARGET "target" + +/** Sent when a mob wants to move or stop. + * @param dir integer: 0 to stop, NORTH/SOUTH/WEST/EAST/etc to move in that direction. + * @param loc /turf: Specify to move in the direction of that turf. + */ +#define COMSIG_MOVE "move" + + +/** BLURB + * @param temp decimal: Adds value to body temperature + */ +#define COMSIG_ADJUST_BODYTEMP "add body temp" // DONE, NEEDS IMPL + + +/** BLURB + * @param amount decimal: Adjust bruteloss by the given amount. + */ +#define COMSIG_ADJUST_BRUTE "adjust brute loss" // DONE, NEEDS IMPL + + +/** BLURB + * @param target /atom: The target being attacked. + */ +#define COMSIG_ATTACKING "attacking target" // DONE + + +/** BLURB + * @param state boolean: Busy if true. + */ +#define COMSIG_BUSY "busy" // DONE, NEEDS IMPL + + +/** BLURB + * @param temp decimal: Sets body temperature to provided value, in kelvin. + */ +#define COMSIG_SET_BODYTEMP "body temp" // DONE, NEEDS IMPL + +/** Sent when a component is added to the container. + * @param component /datum/component: Component being added. + */ +#define COMSIG_COMPONENT_ADDED "component added" + +/** Sent when a component is being removed from the container. + * @param component /datum/component: Component being removed. + */ +#define COMSIG_COMPONENT_REMOVING "component removing" diff --git a/code/__HELPERS/cmp.dm b/code/__HELPERS/cmp.dm index a4c89cdfe4f..472f906968a 100644 --- a/code/__HELPERS/cmp.dm +++ b/code/__HELPERS/cmp.dm @@ -39,3 +39,10 @@ var/cmp_field = "name" /proc/cmp_subsystem_priority(datum/subsystem/a, datum/subsystem/b) return a.priority - b.priority + +var/atom/cmp_dist_origin=null +/proc/cmp_dist_asc(var/atom/a, var/atom/b) + return get_dist_squared(cmp_dist_origin, a) - get_dist_squared(cmp_dist_origin, b) + +/proc/cmp_dist_desc(var/atom/a, var/atom/b) + return get_dist_squared(cmp_dist_origin, b) - get_dist_squared(cmp_dist_origin, a) diff --git a/code/__HELPERS/maths.dm b/code/__HELPERS/maths.dm index 9b608531bd7..8fb757e1f21 100644 --- a/code/__HELPERS/maths.dm +++ b/code/__HELPERS/maths.dm @@ -226,6 +226,16 @@ proc/arctan(x) #undef k1 #undef k2 +/** + * Get Distance, Squared + * + * Because sqrt is slow, this returns the distance squared, which skips the sqrt step. + * + * Use to compare distances. Used in component mobs. + */ +/proc/get_dist_squared(var/atom/a, var/atom/b) + return ((b.x-a.x)**2) + ((b.y-a.y)**2) + //Checks if something's a power of 2, to check bitflags. //Thanks to wwjnc for this. /proc/test_bitflag(var/bitflag) diff --git a/code/modules/components/ai/ai_component.dm b/code/modules/components/ai/ai_component.dm new file mode 100644 index 00000000000..10bf6b14ec2 --- /dev/null +++ b/code/modules/components/ai/ai_component.dm @@ -0,0 +1,13 @@ +/datum/component/ai + var/datum/component/controller/controller + + var/state=0 // AI_STATE_* of the AI. + +/datum/component/ai/RecieveSignal(var/message_type, var/list/args) + switch(message_type) + if(COMSIG_STATE) // list("name"="statename") + state = args["name"] + +/datum/component/ai/New(var/datum/component_container/CC) + ..(CC) + controller=GetComponent(/datum/component/controller) diff --git a/code/modules/components/ai/atmos.dm b/code/modules/components/ai/atmos.dm new file mode 100644 index 00000000000..d638df22737 --- /dev/null +++ b/code/modules/components/ai/atmos.dm @@ -0,0 +1,97 @@ +/datum/component/ai/atmos_checker + //Atmos effect - Yes, you can make creatures that require plasma or co2 to survive. N2O is a trace gas and handled separately, hence why it isn't here. It'd be hard to add it. Hard and me don't mix (Yes, yes make all the dick jokes you want with that.) - Errorage + var/min_oxy = 5 + var/max_oxy = 0 //Leaving something at 0 means it's off - has no maximum + var/min_tox = 0 + var/max_tox = 1 + var/min_co2 = 0 + var/max_co2 = 5 + var/min_n2 = 0 + var/max_n2 = 0 + var/unsuitable_damage = 2 //This damage is taken when atmos doesn't fit all the requirements above + + var/minbodytemp = 250 + var/maxbodytemp = 350 + var/heat_damage_per_tick = 3 //amount of damage applied if animal's body temperature is higher than maxbodytemp + var/cold_damage_per_tick = 2 //same as heat_damage_per_tick, only if the bodytemperature it's lower than minbodytemp + var/fire_alert = 0 + var/oxygen_alert = 0 + var/toxins_alert = 0 + + var/min_overheat_temp=40 + +/datum/component/ai/atmos_checker/RecieveSignal(var/message_type, var/list/args) + switch(message_type) + if("life") + OnLife() + else + ..(message_type, args) + +/datum/component/ai/atmos_checker/proc/OnLife() + if(!isliving(container.holder)) + return 1 + if(container.holder & INVULNERABLE) + return 1 + + var/atmos_suitable = 1 + + var/atom/A = container.holder.loc + + if(isturf(A)) + var/turf/T = A + var/datum/gas_mixture/Environment = T.return_air() + + if(Environment) + if(abs(Environment.temperature - controller.getBodyTemperature()) > min_overheat_temp) + SendSignal(COMSIG_ADJUST_BODYTEMP, list("temp"=((Environment.temperature - controller.getBodyTemperature()) / 5))) + + if(min_oxy) + if(Environment.oxygen < min_oxy) + atmos_suitable = 0 + oxygen_alert = 1 + else + oxygen_alert = 0 + + if(max_oxy) + if(Environment.oxygen > max_oxy) + atmos_suitable = 0 + + if(min_tox) + if(Environment.toxins < min_tox) + atmos_suitable = 0 + + if(max_tox) + if(Environment.toxins > max_tox) + atmos_suitable = 0 + toxins_alert = 1 + else + toxins_alert = 0 + + if(min_n2) + if(Environment.nitrogen < min_n2) + atmos_suitable = 0 + + if(max_n2) + if(Environment.nitrogen > max_n2) + atmos_suitable = 0 + + if(min_co2) + if(Environment.carbon_dioxide < min_co2) + atmos_suitable = 0 + + if(max_co2) + if(Environment.carbon_dioxide > max_co2) + atmos_suitable = 0 + + //Atmos effect + if(controller.getBodyTemperature() < minbodytemp) + fire_alert = 2 + SendSignal(COMSIG_ADJUST_BRUTE, list("amount"=cold_damage_per_tick)) + else if(controller.getBodyTemperature() > maxbodytemp) + fire_alert = 1 + SendSignal(COMSIG_ADJUST_BRUTE, list("amount"=heat_damage_per_tick)) + else + fire_alert = 0 + + if(!atmos_suitable) + SendSignal(COMSIG_ADJUST_BRUTE, list("amount"=unsuitable_damage)) diff --git a/code/modules/components/ai/controller.dm b/code/modules/components/ai/controller.dm new file mode 100644 index 00000000000..f94dd242f4e --- /dev/null +++ b/code/modules/components/ai/controller.dm @@ -0,0 +1,64 @@ +/datum/component/controller + var/atom/holder + + var/_busy=FALSE + var/atom/_target=null + + var/_state=HOSTILE_STANCE_IDLE + +/datum/component/controller/New(var/datum/component_container/container, var/atom/_holder) + ..(container) + holder=_holder + +// Called when we are bumped by another movable atom. +/datum/component/controller/proc/OnBumped(var/atom/A) + if(istype(A, /atom/movable)) + var/atom/movable/AM = A + SendSignal(COMSIG_BUMPED, list("movable"=AM)) + +// Called when we bump another movable atom. +/datum/component/controller/proc/OnBump(var/atom/A) + if(istype(A, /atom/movable)) + var/atom/movable/AM = A + SendSignal(COMSIG_BUMP, list("movable"=AM)) + +// Called when we receive the Life() tick from the MC/scheduler/whatever +/datum/component/controller/proc/Life() + SendSignal(COMSIG_LIFE, list()) + +//* Mob calls these to send signals to components. */ +/datum/component/controller/proc/AttackTarget(var/atom/A) + container.SendSignalToFirst(/datum/component/ai, COMSIG_ATTACKING, list("target"=A)) + +/datum/component/controller/proc/setBusy(var/yes) + _busy = yes + SendSignal(COMSIG_BUSY, list("state"=_busy)) + +/datum/component/controller/proc/getBusy() + return _busy + +/datum/component/controller/proc/setTarget(var/atom/A) + _target = A + SendSignal(COMSIG_TARGET, list("target"=_target)) + +/datum/component/controller/proc/getTarget() + return _target + +/datum/component/controller/proc/setState(var/new_state) + _state = new_state + SendSignal(COMSIG_STATE, list("state"=_state)) + +/datum/component/controller/proc/getState() + return _state + +/datum/component/controller/proc/setBodyTemperature(var/temp) + SendSignal(COMSIG_SET_BODYTEMP, list("temp"=temp,"from"=src)) + +/datum/component/controller/proc/getBodyTemperature() + return -1 + +/datum/component/controller/proc/canAttack(var/atom/A) + if(istype(container.holder, /mob/living/simple_animal)) + var/mob/living/simple_animal/SA = container.holder + return SA.CanAttack(A) + return FALSE diff --git a/code/modules/components/ai/controllers/mob_controller.dm b/code/modules/components/ai/controllers/mob_controller.dm new file mode 100644 index 00000000000..5c51d6e71db --- /dev/null +++ b/code/modules/components/ai/controllers/mob_controller.dm @@ -0,0 +1,26 @@ +/datum/component/controller/mob + var/walk_delay=4 + +/datum/component/controller/mob/RecieveSignal(var/message_type, var/list/args) + if(isliving(container.holder)) + var/mob/living/M=container.holder + //testing("Got command: \[[message_type]\]: [json_encode(args)]") + switch(message_type) + if(COMSIG_MOVE) // list("loc"=turf) + // list("dir"=NORTH) + if("loc" in args) + //walk_to(src, target, minimum_distance, delay) + //testing("Walking towards [args["loc"]] with walk_delay=[walk_delay]") + walk_to(M, args["loc"], 1, walk_delay) + if("dir" in args) + // walk(M, get_dir(src,M), MISSILE_SPEED) + walk(M, args["dir"], walk_delay) + + if(COMSIG_ADJUST_BODYTEMP) // list("temp"=TEMP_IN_KELVIN) + M.bodytemperature += args["temp"] + + if(COMSIG_SET_BODYTEMP) // list("temp"=TEMP_IN_KELVIN) + M.bodytemperature = args["temp"] + + if(COMSIG_STATE) // list("state"=HOSTILE_STANCE_ATTACK) + setState(args["state"]) diff --git a/code/modules/components/ai/controllers/simple_animal.dm b/code/modules/components/ai/controllers/simple_animal.dm new file mode 100644 index 00000000000..f1f88179e3a --- /dev/null +++ b/code/modules/components/ai/controllers/simple_animal.dm @@ -0,0 +1,8 @@ +/datum/component/controller/simple_animal + var/disable_automove_on_busy=1 + +/datum/component/controller/simple_animal/setBusy(var/yes) + ..(yes) + if(disable_automove_on_busy) + var/mob/living/simple_animal/SA = holder + SA.stop_automated_movement = yes diff --git a/code/modules/components/ai/door_opener.dm b/code/modules/components/ai/door_opener.dm new file mode 100644 index 00000000000..58aa778a941 --- /dev/null +++ b/code/modules/components/ai/door_opener.dm @@ -0,0 +1,78 @@ +/datum/component/ai/door_opener + var/pressure_check=TRUE + var/max_pressure_diff=-1 + +/datum/component/ai/door_opener/RecieveSignal(var/message_type, var/list/args) + switch(message_type) + if(COMSIG_ATTACKING) // list("target"=A) + OnAttackingTarget(args["target"]) + else + ..(message_type, args) + +/datum/component/ai/door_opener/proc/OnAttackingTarget(var/atom/target) + if(istype(target,/obj/machinery/door)) + var/obj/machinery/door/D = target + if(CanOpenDoor(D)) + if(get_dist(src, target) > 1) + return // keep movin'. + controller.setBusy(TRUE) + SendSignal(COMSIG_MOVE, "dir"=0) // Stop movement? + D.visible_message("\The [D]'s motors whine as four arachnid claws begin trying to force it open!") + spawn(50) + if(CanOpenDoor(D) && prob(25)) + D.open(1) + D.visible_message("\The [src] forces \the [D] open!") + + // Open firedoors, too. + for(var/obj/machinery/door/firedoor/FD in D.loc) + if(FD && FD.density) + FD.open(1) + + // Reset targetting + controller.setBusy(FALSE) + controller.setTarget(null) + return + controller.setBusy(FALSE) + return + +/datum/component/ai/door_opener/proc/performPressureCheck(var/turf/loc) + var/turf/simulated/lT=loc + if(!istype(lT) || !lT.zone) + return 0 + var/datum/gas_mixture/myenv=lT.return_air() + var/pressure=myenv.return_pressure() + + for(var/dir in cardinal) + var/turf/simulated/T=get_turf(get_step(loc,dir)) + if(T && istype(T) && T.zone) + var/datum/gas_mixture/environment = T.return_air() + var/pdiff = abs(pressure - environment.return_pressure()) + if(pdiff > max_pressure_diff) + return pdiff + return 0 + +/datum/component/ai/door_opener/proc/CanOpenDoor(var/obj/machinery/door/D) + if(istype(D,/obj/machinery/door/poddoor) || istype(D, /obj/machinery/door/airlock/multi_tile/glass)) + return 0 + + // Don't fuck with doors that are doing something + if(D.operating>0) + return 0 + + // Don't open opened doors. + if(!D.density) + return 0 + + // Can't open bolted/welded doors + if(istype(D,/obj/machinery/door/airlock)) + var/obj/machinery/door/airlock/A=D + if(A.locked || A.welded || A.jammed) + return 0 + + var/turf/T = get_turf(D) + + // Don't kill ourselves + if(max_pressure_diff > -1 && !performPressureCheck(T)) + return 0 + + return 1 diff --git a/code/modules/components/ai/hostile/escape_confinement.dm b/code/modules/components/ai/hostile/escape_confinement.dm new file mode 100644 index 00000000000..6fe04952fd6 --- /dev/null +++ b/code/modules/components/ai/hostile/escape_confinement.dm @@ -0,0 +1,51 @@ +/datum/component/ai/escape_confinement + var/life_tick=0 + +/datum/component/ai/escape_confinement/RecieveSignal(var/message_type, var/list/args) + switch(message_type) + if(COMSIG_LIFE) + OnLife() + +/datum/component/ai/escape_confinement/proc/OnLife() + life_tick++ + var/mob/M = container.holder + if(!controller) + controller = GetComponent(/datum/component/controller) + if(controller.getBusy()) + return + switch(controller.getState()) + if(HOSTILE_STANCE_IDLE) + EscapeConfinement() + if(HOSTILE_STANCE_ATTACK) + if(!(M.flags & INVULNERABLE)) + DestroySurroundings() + if(HOSTILE_STANCE_ATTACKING) + if(!(M.flags & INVULNERABLE)) + DestroySurroundings() + +/datum/component/ai/escape_confinement/proc/EscapeConfinement() + var/atom/A = container.holder + if(istype(A, /mob)) + var/mob/M = A + if(M.locked_to) + M.locked_to.attack_animal(A) + if(!isturf(A.loc) && A.loc != null)//Did someone put us in something? + var/atom/locA = A.loc + locA.attack_animal(A)//Bang on it till we get out + +/datum/component/ai/escape_confinement/proc/DestroySurroundings() + EscapeConfinement() + var/list/smash_dirs = list(0) + var/atom/target = controller.getTarget() + if(!target || !controller.canAttack(target)) + smash_dirs |= alldirs //if no target, attack everywhere + else + var/targdir = get_dir(src, target) + smash_dirs |= widen_dir(targdir) //otherwise smash towards the target + for(var/dir in smash_dirs) + var/turf/T = get_step(src, dir) + if(istype(T, /turf/simulated/wall) && container.holder.Adjacent(T)) + T.attack_animal(src) + for(var/atom/A in T) + if((istype(A, /obj/structure/window) || istype(A, /obj/structure/closet) || istype(A, /obj/structure/table) || istype(A, /obj/structure/grille) || istype(A, /obj/structure/rack)) && container.holder.Adjacent(A)) + A.attack_animal(src) diff --git a/code/modules/components/ai/hostile/hunt.dm b/code/modules/components/ai/hostile/hunt.dm new file mode 100644 index 00000000000..5c65b84561f --- /dev/null +++ b/code/modules/components/ai/hostile/hunt.dm @@ -0,0 +1,53 @@ +// Hunting controller from spiders +/datum/component/ai/hunt + var/last_dir=0 // cardinal direction + var/last_was_bumped=0 // Boolean, indicates whether the last movement resulted in a Bump(). + var/life_tick=0 + + var/movement_range=20 // Maximum range of points we move to (20 in spiders) + + var/targetfind_delay=10 + var/datum/component/ai/target_holder/target_holder = null + +/datum/component/ai/hunt/RecieveSignal(var/message_type, var/list/args) + switch(message_type) + if(COMSIG_LIFE) // no arguments + OnLife() + + if(COMSIG_BUMPED) // list("movable"=AM) + OnBumped(args["movable"]) + +/datum/component/ai/hunt/proc/OnLife() + life_tick++ + //testing("HUNT LIFE, controller=[!isnull(controller)], busy=[controller && controller.getBusy()], state=[controller && controller.getState()]") + if(!target_holder) + target_holder = GetComponent(/datum/component/ai/target_holder) + if(!controller) + controller = GetComponent(/datum/component/controller) + if(controller.getBusy()) + return + switch(controller.getState()) + if(HOSTILE_STANCE_IDLE) + var/atom/target = target_holder.GetBestTarget(src, "target_evaluator") + //testing(" IDLE STANCE, target=\ref[target]") + if(!isnull(target)) + SendSignal(COMSIG_TARGET, list("target"=target)) + SendSignal(COMSIG_STATE, list("state"=HOSTILE_STANCE_ATTACK)) + else + SendSignal(COMSIG_MOVE, list("loc" = pick(orange(movement_range, src)))) + if(HOSTILE_STANCE_ATTACK) + var/atom/target = target_holder.GetBestTarget(src, "target_evaluator") + //testing(" ATTACK STANCE, target=\ref[target]") + if(!isnull(target)) + var/turf/T = get_turf(target) + container.SendSignalToFirst(/datum/component/ai, COMSIG_ATTACKING, list("target"=target)) // We're telling the attack modules that we have attack intention. They then individually decide whether to fire. + if(T) + SendSignal(COMSIG_MOVE, list("loc" = T)) + return + SendSignal(COMSIG_STATE, list("state"=HOSTILE_STANCE_IDLE)) // Lost target + +/datum/component/ai/hunt/proc/OnBumped(var/atom/movable/AM) + // TODO + +/datum/component/ai/hunt/proc/target_evaluator(var/atom/target) + return TRUE diff --git a/code/modules/components/ai/hostile/melee/attack_animal.dm b/code/modules/components/ai/hostile/melee/attack_animal.dm new file mode 100644 index 00000000000..a072e78474c --- /dev/null +++ b/code/modules/components/ai/hostile/melee/attack_animal.dm @@ -0,0 +1,7 @@ +// This just calls animal_attack() on stuff. +/datum/component/ai/melee/attack_animal/OnAttackingTarget(var/atom/target) + if(..(target)) + var/mob/living/L = target + L.attack_animal(container.holder) + return 1 // Accepted + return 0 // Unaccepted diff --git a/code/modules/components/ai/hostile/melee/inject_reagent.dm b/code/modules/components/ai/hostile/melee/inject_reagent.dm new file mode 100644 index 00000000000..0f9b6074250 --- /dev/null +++ b/code/modules/components/ai/hostile/melee/inject_reagent.dm @@ -0,0 +1,19 @@ +/datum/component/ai/melee/inject_reagent + var/poison_type = "" // STOXIN, etc + var/poison_per_bite = 0 // Mols to inject + var/inject_prob = 0 // Chance to inject, -1 = ALWAYS + var/max_poison = 0 // Maximum mols in target's blood. 0 = INF + +/datum/component/ai/melee/inject_reagent/OnAttackingTarget(var/atom/target) + if(..(target)) + var/mob/living/L = target + if(L.reagents) + if(inject_prob == -1 || prob(inject_prob)) + var/curamt = L.reagents.get_reagent_amount(poison_type) + var/newamt = max_poison - curamt + if(newamt >= 1) + // TEXT-FORMATTING FUNCTIONS WHEN BYOND? + container.holder.visible_message("\The [src] injects something into \the [target]!") + L.reagents.add_reagent(poison_type, poison_per_bite) + return 1 // Accepted signal + return 0 // Did not accept signal diff --git a/code/modules/components/ai/hostile/melee/melee.dm b/code/modules/components/ai/hostile/melee/melee.dm new file mode 100644 index 00000000000..81b555087b3 --- /dev/null +++ b/code/modules/components/ai/hostile/melee/melee.dm @@ -0,0 +1,14 @@ + + +/datum/component/ai/melee/RecieveSignal(var/message_type, var/list/args) + switch(message_type) + if(COMSIG_ATTACKING) // list("target"=A) + return OnAttackingTarget(args["target"]) + else + return ..(message_type, args) + +/datum/component/ai/melee/proc/OnAttackingTarget(var/atom/target) + if(!isliving(target)) + return 0 + var/mob/living/L = target + return L.Adjacent(container.holder) diff --git a/code/modules/components/ai/mobs/component_mob.dm b/code/modules/components/ai/mobs/component_mob.dm new file mode 100644 index 00000000000..3a19dc7ebe6 --- /dev/null +++ b/code/modules/components/ai/mobs/component_mob.dm @@ -0,0 +1,25 @@ +/** + * Component-driven mob. + * + * See /datum/component and /datum/component_container. + */ +/mob/living/component + var/datum/component_container/container + +/mob/living/component/New() + ..() + container = new (src) + InitializeComponents() + +/mob/living/component/proc/InitializeComponents() + // Set up components here + //var/datum/component/.../ref = container.AddComponent(/datum/component/...) + +/mob/living/component/Life() + ..() + container.SendSignal(COMSIG_LIFE,list()) + + +/mob/living/component/Destroy() + qdel(container) + ..() diff --git a/code/modules/components/ai/mobs/giant_spider.dm b/code/modules/components/ai/mobs/giant_spider.dm new file mode 100644 index 00000000000..0d48a5d7198 --- /dev/null +++ b/code/modules/components/ai/mobs/giant_spider.dm @@ -0,0 +1,22 @@ +/mob/living/component/giant_spider + name="component giant spider" + + icon_state = "guard" + icon = 'icons/mob/animal.dmi' + +/mob/living/component/giant_spider/InitializeComponents() + container.AddComponent(/datum/component/controller/mob) + container.AddComponent(/datum/component/ai/escape_confinement) + container.AddComponent(/datum/component/ai/hunt) + container.AddComponent(/datum/component/ai/melee/attack_animal) + var/datum/component/ai/melee/inject_reagent/injector = container.AddComponent(/datum/component/ai/melee/inject_reagent) + injector.poison_type = STOXIN + injector.poison_per_bite = 5 + var/datum/component/ai/target_finder/simple_view/sv = container.AddComponent(/datum/component/ai/target_finder/simple_view) + sv.range = 5 + // These two should probably be done on New() based on container.holder. + sv.exclude_types += src.type + sv.exclude_types += /mob/living/silicon/robot/mommi // Because we wuv dem + var/datum/component/ai/target_holder/prioritizing/th = container.AddComponent(/datum/component/ai/target_holder/prioritizing) + th.type_priorities[src.type]=0 + container.AddComponent(/datum/component/ai/door_opener) diff --git a/code/modules/components/ai/target_finders/simple_view.dm b/code/modules/components/ai/target_finders/simple_view.dm new file mode 100644 index 00000000000..4dfa7b4f0f1 --- /dev/null +++ b/code/modules/components/ai/target_finders/simple_view.dm @@ -0,0 +1,8 @@ +/datum/component/ai/target_finder/simple_view/GetTargets() + ASSERT(container.holder!=null) + var/list/o = list() + for(var/atom/A in view(range, container.holder)) + if(is_type_in_list(A, exclude_types)) + continue + o += A + return o diff --git a/code/modules/components/ai/target_finders/target_finder.dm b/code/modules/components/ai/target_finders/target_finder.dm new file mode 100644 index 00000000000..9540bea87e6 --- /dev/null +++ b/code/modules/components/ai/target_finders/target_finder.dm @@ -0,0 +1,14 @@ +/* +var/datum/controller/target_finder/TF = GetComponent(/datum/controller/target_finder) +var/list/target = TF.GetTargets() +*/ +/datum/component/ai/target_finder + var/range=0 + var/list/exclude_types=list( + /obj/effect, + /atom/movable/lighting_overlay, + /turf + ) + +/datum/component/ai/target_finder/proc/GetTargets() + return list() diff --git a/code/modules/components/ai/target_holders/prioritizing_holder.dm b/code/modules/components/ai/target_holders/prioritizing_holder.dm new file mode 100644 index 00000000000..d4d02ab4c88 --- /dev/null +++ b/code/modules/components/ai/target_holders/prioritizing_holder.dm @@ -0,0 +1,55 @@ +/datum/component/ai/target_holder/prioritizing + var/list/targets= list() + var/list/type_priorities = list( + /mob/living = 1, + // /mob/living/simple_animal/hostile/giant_spider=0, + /obj/machinery/door = 2, + /obj/machinery/light = 3 + ) + var/default_priority=2 + + var/max_priority=3 + + var/datum/component/ai/target_finder/finder = null + +/datum/component/ai/target_holder/prioritizing/proc/attach() + if(!finder) + finder = GetComponent(/datum/component/ai/target_finder) + +/datum/component/ai/target_holder/prioritizing/AddTarget(var/atom/A) + var/priority=-1 + for(var/priority_type in type_priorities) + if(istype(A, priority_type)) + priority = type_priorities[priority_type] + break + if(priority==-1) // Use default + priority=default_priority + if(priority==0) // Don't add + return + if(!("[priority]" in targets)) + targets["[priority]"] = list() + if(!(A in targets["[priority]"])) + targets["[priority]"] += A + +/datum/component/ai/target_holder/prioritizing/RemoveTarget(var/atom/A) + for(var/priority in targets) + if(A in targets[priority]) + targets[priority] -= A + +/datum/component/ai/target_holder/prioritizing/GetBestTarget(var/objRef, var/procName, var/from_finder=1) + if(from_finder) + attach() + targets.Cut() // Clear first + //var/n=0 + for(var/atom/target in finder.GetTargets()) + AddTarget(target) + //n++ + //testing(" TH: Got [n] targets from TF") + for(var/priority=1;priority<=max_priority;priority++) + var/list/priority_targets = targets["[priority]"] + if(priority_targets == null) + continue + for(var/atom/target in sortMerge(priority_targets, cmp=/proc/cmp_dist_asc, associative=0, fromIndex=1, toIndex=priority_targets.len)) + if(call(objRef, procName)(target)) + return target + return null diff --git a/code/modules/components/ai/target_holders/target_holder.dm b/code/modules/components/ai/target_holders/target_holder.dm new file mode 100644 index 00000000000..0d31d2332f0 --- /dev/null +++ b/code/modules/components/ai/target_holders/target_holder.dm @@ -0,0 +1,19 @@ +/datum/component/ai/target_holder + +/datum/component/ai/target_holder/proc/AddTarget(var/atom/A) + return + +/datum/component/ai/target_holder/proc/RemoveTarget(var/atom/A) + return + + +/** + * Get the best target + * + * @param objRef Direct reference to the holding object containing the target validation callback + * @param procName Name of the callback proc + * @param from_finder Use /datum/component/ai/target_finder.GetTargets() + * @return null if not target found, /atom if a target is found. + */ +/datum/component/ai/target_holder/proc/GetBestTarget(var/objRef, var/procName, var/from_finder=1) + return diff --git a/code/modules/components/component.dm b/code/modules/components/component.dm new file mode 100644 index 00000000000..a4e29bd65b2 --- /dev/null +++ b/code/modules/components/component.dm @@ -0,0 +1,24 @@ +/datum/component + var/datum/component_container/container + + // Enables or disables the components + var/enabled=1 + +/datum/component/New(var/datum/component_container/CC) + container=CC + +// Override to receive signals. +/datum/component/proc/RecieveSignal(var/sigtype, var/list/args) + return + +// Send a signal to all other components in the container. +/datum/component/proc/SendSignal(var/sigtype, var/list/args) + container.SendSignal(sigtype, args) + +// Return first /datum/component that is subtype of c_type. +/datum/component/proc/GetComponent(var/c_type) + return container.GetComponent(c_type) + +// Returns ALL /datum/components in parent container that are subtypes of c_type. +/datum/component/proc/GetComponents(var/c_type) + return container.GetComponents(c_type) diff --git a/code/modules/components/component_container.dm b/code/modules/components/component_container.dm new file mode 100644 index 00000000000..06e7579f554 --- /dev/null +++ b/code/modules/components/component_container.dm @@ -0,0 +1,99 @@ +// Basic multipurpose component container. +/datum/component_container + + // Where the components themselves are stored after initialization. + var/list/components=list() + + // The types to initialize the holder with. Later, holds unique list of all types in container. + var/list/types=list() + + // The entity that holds this datum. + var/atom/holder + + // Used for dependency management. + +/datum/component_container/New(var/atom/holder_atom) + holder=holder_atom + +/datum/component_container/proc/AddComponentsByType(var/list/new_types) + var/list/newtypes=list() + for(var/new_type in new_types) + AddComponent(new_type,TRUE) + if(!(new_type in newtypes)) + newtypes.Add(new_type) + types=newtypes + +/** + * Add component to the container. + * + * @param new_type The type we wish to instantiate in the component_container. + * @param initializing Do not use, only used in container New() for internal purposes. + */ +/datum/component_container/proc/AddComponent(var/new_type, var/initializing=FALSE) + if(!initializing && !(new_type in types)) + types.Add(new_type) + var/datum/component/C = new new_type(src) + components.Add(C) + SendSignal(COMSIG_COMPONENT_ADDED,list("component"=C)) + return C + +/** + * Removes a component from the container. + * + * @param C The component to remove + */ +/datum/component_container/proc/RemoveComponent(var/datum/component/C) + SendSignal(COMSIG_COMPONENT_REMOVING,list("component"=C)) + components.Remove(C) + types.Cut() + for(var/datum/component/CC in components) + if(!(CC.type in types)) + types.Add(CC.type) + //qdel(C) Will most likely get GC'd anyway. + +/** + * Send a signal to all components in the container. + * + * @param message_type Name of the signal. + * @param args List of arguments to send with the signal. + */ +/datum/component_container/proc/SendSignal(var/message_type, var/list/args) + for(var/datum/component/C in components) + if(C.enabled) + C.RecieveSignal(message_type, args) + +/** + * Send a signal to the first component of type accepting a signal. + * + * @param component_type + * @param message_type Name of the signal. + * @param args List of arguments to send with the signal. + */ +/datum/component_container/proc/SendSignalToFirst(var/desired_type, var/message_type, var/list/args, var/shuffle=TRUE) + var/list/shuffled=list(components) // Copy list so we don't disorder the container. + if(shuffle) + shuffled=shuffle(shuffled) + for(var/datum/component/C in components) + if(C.enabled && istype(C, desired_type)) + if(C.RecieveSignal(message_type, args)) // return 1 to accept signal. + return + + +/** + * Get the first component matching the specified type. + */ +/datum/component_container/proc/GetComponent(var/desired_type) + for(var/datum/component/C in components) + if(istype(C, desired_type)) + return C + return null + +/** + * Get the all components matching the specified type. + */ +/datum/component_container/proc/GetComponents(var/desired_type) + . = list() + for(var/datum/component/C in components) + if(istype(C, desired_type)) + . += C + return . diff --git a/code/modules/mob/living/living.dm b/code/modules/mob/living/living.dm index dd2fd6b5ee0..ebc9cd7b68d 100644 --- a/code/modules/mob/living/living.dm +++ b/code/modules/mob/living/living.dm @@ -76,9 +76,10 @@ if (flags & INVULNERABLE) bodytemperature = initial(bodytemperature) if (monkeyizing) - return + return 0 if(!loc) - return // Fixing a null error that occurs when the mob isn't found in the world -- TLE + return 0 // Fixing a null error that occurs when the mob isn't found in the world -- TLE + // Why the fuck is this handled here? if(reagents && reagents.has_reagent(BUSTANUT)) if(!(M_HARDCORE in mutations)) mutations.Add(M_HARDCORE) @@ -99,7 +100,7 @@ if(mind) if(mind in ticker.mode.implanted) if(implanting) - return + return 0 // to_chat(world, "[src.name]") var/datum/mind/head = ticker.mode.implanted[mind] //var/list/removal @@ -115,6 +116,7 @@ special_role = null to_chat(current, "The fog clouding your mind clears. You remember nothing from the moment you were implanted until now..(You don't remember who enslaved you)") */ + return 1 // Apply connect damage /mob/living/beam_connect(var/obj/effect/beam/B) diff --git a/vgstation13.dme b/vgstation13.dme index 64e4ccae264..9fbc14630a3 100644 --- a/vgstation13.dme +++ b/vgstation13.dme @@ -36,6 +36,7 @@ #include "code\hub.dm" #include "code\names.dm" #include "code\world.dm" +#include "code\__DEFINES\component_signals.dm" #include "code\__DEFINES\disease2.dm" #include "code\__DEFINES\hydro.dm" #include "code\__DEFINES\pai_software.dm" @@ -1132,6 +1133,25 @@ #include "code\modules\clothing\under\jobs\engineering.dm" #include "code\modules\clothing\under\jobs\medsci.dm" #include "code\modules\clothing\under\jobs\security.dm" +#include "code\modules\components\component.dm" +#include "code\modules\components\component_container.dm" +#include "code\modules\components\ai\ai_component.dm" +#include "code\modules\components\ai\atmos.dm" +#include "code\modules\components\ai\controller.dm" +#include "code\modules\components\ai\door_opener.dm" +#include "code\modules\components\ai\controllers\mob_controller.dm" +#include "code\modules\components\ai\controllers\simple_animal.dm" +#include "code\modules\components\ai\hostile\escape_confinement.dm" +#include "code\modules\components\ai\hostile\hunt.dm" +#include "code\modules\components\ai\hostile\melee\attack_animal.dm" +#include "code\modules\components\ai\hostile\melee\inject_reagent.dm" +#include "code\modules\components\ai\hostile\melee\melee.dm" +#include "code\modules\components\ai\mobs\component_mob.dm" +#include "code\modules\components\ai\mobs\giant_spider.dm" +#include "code\modules\components\ai\target_finders\simple_view.dm" +#include "code\modules\components\ai\target_finders\target_finder.dm" +#include "code\modules\components\ai\target_holders\prioritizing_holder.dm" +#include "code\modules\components\ai\target_holders\target_holder.dm" #include "code\modules\context_click\context_click.dm" #include "code\modules\context_click\remote.dm" #include "code\modules\customitems\item_spawning.dm"