mirror of
https://github.com/evennia/evennia.git
synced 2026-04-03 06:27:17 +02:00
428 lines
16 KiB
Python
428 lines
16 KiB
Python
"""
|
|
This module implements a simple mobile object with
|
|
a very rudimentary AI as well as an aggressive enemy
|
|
object based on that mobile class.
|
|
|
|
"""
|
|
|
|
import random
|
|
|
|
from evennia import TICKER_HANDLER
|
|
from evennia import search_object
|
|
from evennia import Command, CmdSet
|
|
from evennia import logger
|
|
from evennia.contrib.tutorial_world import objects as tut_objects
|
|
|
|
|
|
class CmdMobOnOff(Command):
|
|
"""
|
|
Activates/deactivates Mob
|
|
|
|
Usage:
|
|
mobon <mob>
|
|
moboff <mob>
|
|
|
|
This turns the mob from active (alive) mode
|
|
to inactive (dead) mode. It is used during
|
|
building to activate the mob once it's
|
|
prepared.
|
|
"""
|
|
key = "mobon"
|
|
aliases = "moboff"
|
|
locks = "cmd:superuser()"
|
|
|
|
def func(self):
|
|
"""
|
|
Uses the mob's set_alive/set_dead methods
|
|
to turn on/off the mob."
|
|
"""
|
|
if not self.args:
|
|
self.caller.msg("Usage: mobon||moboff <mob>")
|
|
return
|
|
mob = self.caller.search(self.args)
|
|
if not mob:
|
|
return
|
|
if self.cmdstring == "mobon":
|
|
mob.set_alive()
|
|
else:
|
|
mob.set_dead()
|
|
|
|
|
|
class MobCmdSet(CmdSet):
|
|
"""
|
|
Holds the admin command controlling the mob
|
|
"""
|
|
|
|
def at_cmdset_creation(self):
|
|
self.add(CmdMobOnOff())
|
|
|
|
|
|
class Mob(tut_objects.TutorialObject):
|
|
"""
|
|
This is a state-machine AI mobile. It has several states which are
|
|
controlled from setting various Attributes. All default to True:
|
|
|
|
patrolling: if set, the mob will move randomly
|
|
from room to room, but preferring to not return
|
|
the way it came. If unset, the mob will remain
|
|
stationary (idling) until attacked.
|
|
aggressive: if set, will attack Characters in
|
|
the same room using whatever Weapon it
|
|
carries (see tutorial_world.objects.Weapon).
|
|
if unset, the mob will never engage in combat
|
|
no matter what.
|
|
hunting: if set, the mob will pursue enemies trying
|
|
to flee from it, so it can enter combat. If unset,
|
|
it will return to patrolling/idling if fled from.
|
|
immortal: If set, the mob cannot take any damage.
|
|
irregular_echoes: list of strings the mob generates at irregular intervals.
|
|
desc_alive: the physical description while alive
|
|
desc_dead: the physical descripion while dead
|
|
send_defeated_to: unique key/alias for location to send defeated enemies to
|
|
defeat_msg: message to echo to defeated opponent
|
|
defeat_msg_room: message to echo to room. Accepts %s as the name of the defeated.
|
|
hit_msg: message to echo when this mob is hit. Accepts %s for the mob's key.
|
|
weapon_ineffective_msg: message to echo for useless attacks
|
|
death_msg: message to echo to room when this mob dies.
|
|
patrolling_pace: how many seconds per tick, when patrolling
|
|
aggressive_pace: -"- attacking
|
|
hunting_pace: -"- hunting
|
|
death_pace: -"- returning to life when dead
|
|
|
|
field 'home' - the home location should set to someplace inside
|
|
the patrolling area. The mob will use this if it should
|
|
happen to roam into a room with no exits.
|
|
|
|
"""
|
|
|
|
def at_init(self):
|
|
"""
|
|
When initialized from cache (after a server reboot), set up
|
|
the AI state.
|
|
"""
|
|
# The AI state machine (not persistent).
|
|
self.ndb.is_patrolling = self.db.patrolling and not self.db.is_dead
|
|
self.ndb.is_attacking = False
|
|
self.ndb.is_hunting = False
|
|
self.ndb.is_immortal = self.db.immortal or self.db.is_dead
|
|
|
|
def at_object_creation(self):
|
|
"""
|
|
Called the first time the object is created.
|
|
We set up the base properties and flags here.
|
|
"""
|
|
self.cmdset.add(MobCmdSet, permanent=True)
|
|
# Main AI flags. We start in dead mode so we don't have to
|
|
# chase the mob around when building.
|
|
self.db.patrolling = True
|
|
self.db.aggressive = True
|
|
self.db.immortal = False
|
|
# db-store if it is dead or not
|
|
self.db.is_dead = True
|
|
# specifies how much damage we divide away from non-magic weapons
|
|
self.db.damage_resistance = 100.0
|
|
# pace (number of seconds between ticks) for
|
|
# the respective modes.
|
|
self.db.patrolling_pace = 6
|
|
self.db.aggressive_pace = 2
|
|
self.db.hunting_pace = 1
|
|
self.db.death_pace = 100 # stay dead for 100 seconds
|
|
|
|
# we store the call to the tickerhandler
|
|
# so we can easily deactivate the last
|
|
# ticker subscription when we switch.
|
|
# since we will use the same idstring
|
|
# throughout we only need to save the
|
|
# previous interval we used.
|
|
self.db.last_ticker_interval = None
|
|
|
|
# store two separate descriptions, one for alive and
|
|
# one for dead (corpse)
|
|
self.db.desc_alive = "This is a moving object."
|
|
self.db.desc_dead = "A dead body."
|
|
|
|
# health stats
|
|
self.db.full_health = 20
|
|
self.db.health = 20
|
|
|
|
# when this mob defeats someone, we move the character off to
|
|
# some other place (Dark Cell in the tutorial).
|
|
self.db.send_defeated_to = "dark cell"
|
|
# text to echo to the defeated foe.
|
|
self.db.defeat_msg = "You fall to the ground."
|
|
self.db.defeat_msg_room = "%s falls to the ground."
|
|
self.db.weapon_ineffective_msg = "Your weapon just passes through your enemy, causing almost no effect!"
|
|
|
|
self.db.death_msg = "After the last hit %s evaporates." % self.key
|
|
self.db.hit_msg = "%s wails, shudders and writhes." % self.key
|
|
self.db.irregular_msgs = ["the enemy looks about.", "the enemy changes stance."]
|
|
|
|
self.db.tutorial_info = "This is an object with simple state AI, using a ticker to move."
|
|
|
|
def _set_ticker(self, interval, hook_key, stop=False):
|
|
"""
|
|
Set how often the given hook key should
|
|
be "ticked".
|
|
|
|
Args:
|
|
interval (int): The number of seconds
|
|
between ticks
|
|
hook_key (str): The name of the method
|
|
(on this mob) to call every interval
|
|
seconds.
|
|
stop (bool, optional): Just stop the
|
|
last ticker without starting a new one.
|
|
With this set, the interval and hook_key
|
|
arguments are unused.
|
|
|
|
In order to only have one ticker
|
|
running at a time, we make sure to store the
|
|
previous ticker subscription so that we can
|
|
easily find and stop it before setting a
|
|
new one. The tickerhandler is persistent so
|
|
we need to remember this across reloads.
|
|
|
|
"""
|
|
idstring = "tutorial_mob" # this doesn't change
|
|
last_interval = self.db.last_ticker_interval
|
|
last_hook_key = self.db.last_hook_key
|
|
if last_interval and last_hook_key:
|
|
# we have a previous subscription, kill this first.
|
|
TICKER_HANDLER.remove(interval=last_interval,
|
|
callback=getattr(self, last_hook_key), idstring=idstring)
|
|
self.db.last_ticker_interval = interval
|
|
self.db.last_hook_key = hook_key
|
|
if not stop:
|
|
# set the new ticker
|
|
TICKER_HANDLER.add(interval=interval,
|
|
callback=getattr(self, hook_key), idstring=idstring)
|
|
|
|
def _find_target(self, location):
|
|
"""
|
|
Scan the given location for suitable targets (this is defined
|
|
as Characters) to attack. Will ignore superusers.
|
|
|
|
Args:
|
|
location (Object): the room to scan.
|
|
|
|
Returns:
|
|
The first suitable target found.
|
|
|
|
"""
|
|
targets = [obj for obj in location.contents_get(exclude=self)
|
|
if obj.has_account and not obj.is_superuser]
|
|
return targets[0] if targets else None
|
|
|
|
def set_alive(self, *args, **kwargs):
|
|
"""
|
|
Set the mob to "alive" mode. This effectively
|
|
resurrects it from the dead state.
|
|
"""
|
|
self.db.health = self.db.full_health
|
|
self.db.is_dead = False
|
|
self.db.desc = self.db.desc_alive
|
|
self.ndb.is_immortal = self.db.immortal
|
|
self.ndb.is_patrolling = self.db.patrolling
|
|
if not self.location:
|
|
self.move_to(self.home)
|
|
if self.db.patrolling:
|
|
self.start_patrolling()
|
|
|
|
def set_dead(self):
|
|
"""
|
|
Set the mob to "dead" mode. This turns it off
|
|
and makes sure it can take no more damage.
|
|
It also starts a ticker for when it will return.
|
|
"""
|
|
self.db.is_dead = True
|
|
self.location = None
|
|
self.ndb.is_patrolling = False
|
|
self.ndb.is_attacking = False
|
|
self.ndb.is_hunting = False
|
|
self.ndb.is_immortal = True
|
|
# we shall return after some time
|
|
self._set_ticker(self.db.death_pace, "set_alive")
|
|
|
|
def start_idle(self):
|
|
"""
|
|
Starts just standing around. This will kill
|
|
the ticker and do nothing more.
|
|
"""
|
|
self._set_ticker(None, None, stop=True)
|
|
|
|
def start_patrolling(self):
|
|
"""
|
|
Start the patrolling state by
|
|
registering us with the ticker-handler
|
|
at a leasurely pace.
|
|
"""
|
|
if not self.db.patrolling:
|
|
self.start_idle()
|
|
return
|
|
self._set_ticker(self.db.patrolling_pace, "do_patrol")
|
|
self.ndb.is_patrolling = True
|
|
self.ndb.is_hunting = False
|
|
self.ndb.is_attacking = False
|
|
# for the tutorial, we also heal the mob in this mode
|
|
self.db.health = self.db.full_health
|
|
|
|
def start_hunting(self):
|
|
"""
|
|
Start the hunting state
|
|
"""
|
|
if not self.db.hunting:
|
|
self.start_patrolling()
|
|
return
|
|
self._set_ticker(self.db.hunting_pace, "do_hunt")
|
|
self.ndb.is_patrolling = False
|
|
self.ndb.is_hunting = True
|
|
self.ndb.is_attacking = False
|
|
|
|
def start_attacking(self):
|
|
"""
|
|
Start the attacking state
|
|
"""
|
|
if not self.db.aggressive:
|
|
self.start_hunting()
|
|
return
|
|
self._set_ticker(self.db.aggressive_pace, "do_attack")
|
|
self.ndb.is_patrolling = False
|
|
self.ndb.is_hunting = False
|
|
self.ndb.is_attacking = True
|
|
|
|
def do_patrol(self, *args, **kwargs):
|
|
"""
|
|
Called repeatedly during patrolling mode. In this mode, the
|
|
mob scans its surroundings and randomly chooses a viable exit.
|
|
One should lock exits with the traverse:has_account() lock in
|
|
order to block the mob from moving outside its area while
|
|
allowing account-controlled characters to move normally.
|
|
"""
|
|
if random.random() < 0.01 and self.db.irregular_msgs:
|
|
self.location.msg_contents(random.choice(self.db.irregular_msgs))
|
|
if self.db.aggressive:
|
|
# first check if there are any targets in the room.
|
|
target = self._find_target(self.location)
|
|
if target:
|
|
self.start_attacking()
|
|
return
|
|
# no target found, look for an exit.
|
|
exits = [exi for exi in self.location.exits
|
|
if exi.access(self, "traverse")]
|
|
if exits:
|
|
# randomly pick an exit
|
|
exit = random.choice(exits)
|
|
# move there.
|
|
self.move_to(exit.destination)
|
|
else:
|
|
# no exits! teleport to home to get away.
|
|
self.move_to(self.home)
|
|
|
|
def do_hunting(self, *args, **kwargs):
|
|
"""
|
|
Called regularly when in hunting mode. In hunting mode the mob
|
|
scans adjacent rooms for enemies and moves towards them to
|
|
attack if possible.
|
|
"""
|
|
if random.random() < 0.01 and self.db.irregular_msgs:
|
|
self.location.msg_contents(random.choice(self.db.irregular_msgs))
|
|
if self.db.aggressive:
|
|
# first check if there are any targets in the room.
|
|
target = self._find_target(self.location)
|
|
if target:
|
|
self.start_attacking()
|
|
return
|
|
# no targets found, scan surrounding rooms
|
|
exits = [exi for exi in self.location.exits
|
|
if exi.access(self, "traverse")]
|
|
if exits:
|
|
# scan the exits destination for targets
|
|
for exit in exits:
|
|
target = self._find_target(exit.destination)
|
|
if target:
|
|
# a target found. Move there.
|
|
self.move_to(exit.destination)
|
|
return
|
|
# if we get to this point we lost our
|
|
# prey. Resume patrolling.
|
|
self.start_patrolling()
|
|
else:
|
|
# no exits! teleport to home to get away.
|
|
self.move_to(self.home)
|
|
|
|
def do_attack(self, *args, **kwargs):
|
|
"""
|
|
Called regularly when in attacking mode. In attacking mode
|
|
the mob will bring its weapons to bear on any targets
|
|
in the room.
|
|
"""
|
|
if random.random() < 0.01 and self.db.irregular_msgs:
|
|
self.location.msg_contents(random.choice(self.db.irregular_msgs))
|
|
# first make sure we have a target
|
|
target = self._find_target(self.location)
|
|
if not target:
|
|
# no target, start looking for one
|
|
self.start_hunting()
|
|
return
|
|
|
|
# we use the same attack commands as defined in
|
|
# tutorial_world.objects.Weapon, assuming that
|
|
# the mob is given a Weapon to attack with.
|
|
attack_cmd = random.choice(("thrust", "pierce", "stab", "slash", "chop"))
|
|
self.execute_cmd("%s %s" % (attack_cmd, target))
|
|
|
|
# analyze the current state
|
|
if target.db.health <= 0:
|
|
# we reduced the target to <= 0 health. Move them to the
|
|
# defeated room
|
|
target.msg(self.db.defeat_msg)
|
|
self.location.msg_contents(self.db.defeat_msg_room % target.key, exclude=target)
|
|
send_defeated_to = search_object(self.db.send_defeated_to)
|
|
if send_defeated_to:
|
|
target.move_to(send_defeated_to[0], quiet=True)
|
|
else:
|
|
logger.log_err("Mob: mob.db.send_defeated_to not found: %s" % self.db.send_defeated_to)
|
|
|
|
# response methods - called by other objects
|
|
|
|
def at_hit(self, weapon, attacker, damage):
|
|
"""
|
|
Someone landed a hit on us. Check our status
|
|
and start attacking if not already doing so.
|
|
"""
|
|
if self.db.health is None:
|
|
# health not set - this can't be damaged.
|
|
attacker.msg(self.db.weapon_ineffective_msg)
|
|
return
|
|
|
|
if not self.ndb.is_immortal:
|
|
if not weapon.db.magic:
|
|
# not a magic weapon - divide away magic resistance
|
|
damage /= self.db.damage_resistance
|
|
attacker.msg(self.db.weapon_ineffective_msg)
|
|
else:
|
|
self.location.msg_contents(self.db.hit_msg)
|
|
self.db.health -= damage
|
|
|
|
# analyze the result
|
|
if self.db.health <= 0:
|
|
# we are dead!
|
|
attacker.msg(self.db.death_msg)
|
|
self.set_dead()
|
|
else:
|
|
# still alive, start attack if not already attacking
|
|
if self.db.aggressive and not self.ndb.is_attacking:
|
|
self.start_attacking()
|
|
|
|
def at_new_arrival(self, new_character):
|
|
"""
|
|
This is triggered whenever a new character enters the room.
|
|
This is called by the TutorialRoom the mob stands in and
|
|
allows it to be aware of changes immediately without needing
|
|
to poll for them all the time. For example, the mob can react
|
|
right away, also when patrolling on a very slow ticker.
|
|
"""
|
|
# the room actually already checked all we need, so
|
|
# we know it is a valid target.
|
|
if self.db.aggressive and not self.ndb.is_attacking:
|
|
self.start_attacking()
|