moss-n-puddles/typeclasses/objects.py
Howard Abrams 8e2918564b Automate the adventure/puzzle
A character can acquire a horn (from stealing it from the witch) and
own the horn (traveling at will). The puzzle now gifts the players who
win a blue medal for their effort.
2025-07-22 21:17:36 -07:00

470 lines
19 KiB
Python
Executable file

#!/usr/bin/env python
"""
Object
The Object is the class for general items in the game world.
Use the ObjectParent class to implement common features for *all* entities
with a location in the game world (like Characters, Rooms, Exits).
"""
from re import split, match, sub, IGNORECASE
from evennia.contrib.rpg.rpsystem.rpsystem import ContribRPObject
from evennia.utils import delay, logger
from utils.word_list import routput
class ObjectParent:
"""
This is a mixin that can be used to override *all* entities inheriting at
some distance from DefaultObject (Objects, Exits, Characters and Rooms).
Just add any method that exists on `DefaultObject` to this class. If one
of the derived classes has itself defined that same hook already, that will
take precedence.
"""
def get_display_footer(self, _, **kwargs):
return "\n"
def has_method(self, method_name):
"""True if this object has a method of a particular name."""
return hasattr(self, method_name) and callable(getattr(self, method_name))
class Object(ObjectParent, ContribRPObject):
"""
This is the root Object typeclass, representing all entities that
have an actual presence in-game. DefaultObjects generally have a
location. They can also be manipulated and looked at. Game
entities you define should inherit from DefaultObject at some distance.
It is recommended to create children of this class using the
`evennia.create_object()` function rather than to initialize the class
directly - this will both set things up and efficiently save the object
without `obj.save()` having to be called explicitly.
Note: Check the autodocs for complete class members, this may not always
be up-to date.
* Base properties defined/available on all Objects
key (string) - name of object
name (string)- same as key
dbref (int, read-only) - unique #id-number. Also "id" can be used.
date_created (string) - time stamp of object creation
account (Account) - controlling account (if any, only set together with
sessid below)
sessid (int, read-only) - session id (if any, only set together with
account above). Use `sessions` handler to get the
Sessions directly.
location (Object) - current location. Is None if this is a room
home (Object) - safety start-location
has_account (bool, read-only)- will only return *connected* accounts
contents (list, read only) - returns all objects inside this object
exits (list of Objects, read-only) - returns all exits from this
object, if any
destination (Object) - only set if this object is an exit.
is_superuser (bool, read-only) - True/False if this user is a superuser
is_connected (bool, read-only) - True if this object is associated with
an Account with any connected sessions.
has_account (bool, read-only) - True is this object has an associated account.
is_superuser (bool, read-only): True if this object has an account and that
account is a superuser.
* Handlers available
aliases - alias-handler: use aliases.add/remove/get() to use.
permissions - permission-handler: use permissions.add/remove() to
add/remove new perms.
locks - lock-handler: use locks.add() to add new lock strings
scripts - script-handler. Add new scripts to object with scripts.add()
cmdset - cmdset-handler. Use cmdset.add() to add new cmdsets to object
nicks - nick-handler. New nicks with nicks.add().
sessions - sessions-handler. Get Sessions connected to this
object with sessions.get()
attributes - attribute-handler. Use attributes.add/remove/get.
db - attribute-handler: Shortcut for attribute-handler. Store/retrieve
database attributes using self.db.myattr=val, val=self.db.myattr
ndb - non-persistent attribute handler: same as db but does not create
a database entry when storing data
* Helper methods (see src.objects.objects.py for full headers)
get_search_query_replacement(searchdata, **kwargs)
get_search_direct_match(searchdata, **kwargs)
get_search_candidates(searchdata, **kwargs)
get_search_result(searchdata, attribute_name=None, typeclass=None,
candidates=None, exact=False, use_dbref=None, tags=None, **kwargs)
get_stacked_result(results, **kwargs)
handle_search_results(searchdata, results, **kwargs)
search(searchdata, global_search=False, use_nicks=True, typeclass=None,
location=None, attribute_name=None, quiet=False, exact=False,
candidates=None, use_locks=True, nofound_string=None,
multimatch_string=None, use_dbref=None, tags=None, stacked=0)
search_account(searchdata, quiet=False)
execute_cmd(raw_string, session=None, **kwargs))
msg(text=None, from_obj=None, session=None, options=None, **kwargs)
for_contents(func, exclude=None, **kwargs)
msg_contents(message, exclude=None, from_obj=None, mapping=None,
raise_funcparse_errors=False, **kwargs)
move_to(destination, quiet=False, emit_to_obj=None, use_destination=True)
clear_contents()
create(key, account, caller, method, **kwargs)
copy(new_key=None)
at_object_post_copy(new_obj, **kwargs)
delete()
is_typeclass(typeclass, exact=False)
swap_typeclass(new_typeclass, clean_attributes=False, no_default=True)
access(accessing_obj, access_type='read', default=False,
no_superuser_bypass=False, **kwargs)
filter_visible(obj_list, looker, **kwargs)
get_default_lockstring()
get_cmdsets(caller, current, **kwargs)
check_permstring(permstring)
get_cmdset_providers()
get_display_name(looker=None, **kwargs)
get_extra_display_name_info(looker=None, **kwargs)
get_numbered_name(count, looker, **kwargs)
get_display_header(looker, **kwargs)
get_display_desc(looker, **kwargs)
get_display_exits(looker, **kwargs)
get_display_characters(looker, **kwargs)
get_display_things(looker, **kwargs)
get_display_footer(looker, **kwargs)
format_appearance(appearance, looker, **kwargs)
return_apperance(looker, **kwargs)
* Hooks (these are class methods, so args should start with self):
basetype_setup() - only called once, used for behind-the-scenes
setup. Normally not modified.
basetype_posthook_setup() - customization in basetype, after the object
has been created; Normally not modified.
at_object_creation() - only called once, when object is first created.
Object customizations go here.
at_object_delete() - called just before deleting an object. If returning
False, deletion is aborted. Note that all objects
inside a deleted object are automatically moved
to their <home>, they don't need to be removed here.
at_init() - called whenever typeclass is cached from memory,
at least once every server restart/reload
at_first_save()
at_cmdset_get(**kwargs) - this is called just before the command handler
requests a cmdset from this object. The kwargs are
not normally used unless the cmdset is created
dynamically (see e.g. Exits).
at_pre_puppet(account)- (account-controlled objects only) called just
before puppeting
at_post_puppet() - (account-controlled objects only) called just
after completing connection account<->object
at_pre_unpuppet() - (account-controlled objects only) called just
before un-puppeting
at_post_unpuppet(account) - (account-controlled objects only) called just
after disconnecting account<->object link
at_server_reload() - called before server is reloaded
at_server_shutdown() - called just before server is fully shut down
at_access(result, accessing_obj, access_type) - called with the result
of a lock access check on this object. Return value
does not affect check result.
at_pre_move(destination) - called just before moving object
to the destination. If returns False, move is cancelled.
announce_move_from(destination) - called in old location, just
before move, if obj.move_to() has quiet=False
announce_move_to(source_location) - called in new location, just
after move, if obj.move_to() has quiet=False
at_post_move(source_location) - always called after a move has
been successfully performed.
at_pre_object_leave(leaving_object, destination, **kwargs)
at_object_leave(obj, target_location, move_type="move", **kwargs)
at_object_leave(obj, target_location) - called when an object leaves
this object in any fashion
at_pre_object_receive(obj, source_location)
at_object_receive(obj, source_location, move_type="move", **kwargs) -
called when this object receives another object
at_post_move(source_location, move_type="move", **kwargs)
at_traverse(traversing_object, target_location, **kwargs) - (exit-objects only)
handles all moving across the exit, including
calling the other exit hooks. Use super() to retain
the default functionality.
at_post_traverse(traversing_object, source_location) - (exit-objects only)
called just after a traversal has happened.
at_failed_traverse(traversing_object) - (exit-objects only) called if
traversal fails and property err_traverse is not defined.
at_msg_receive(self, msg, from_obj=None, **kwargs) - called when a message
(via self.msg()) is sent to this obj.
If returns false, aborts send.
at_msg_send(self, msg, to_obj=None, **kwargs) - called when this objects
sends a message to someone via self.msg().
return_appearance(looker) - describes this object. Used by "look"
command by default
at_desc(looker=None) - called by 'look' whenever the
appearance is requested.
at_pre_get(getter, **kwargs)
at_get(getter) - called after object has been picked up.
Does not stop pickup.
at_pre_give(giver, getter, **kwargs)
at_give(giver, getter, **kwargs)
at_pre_drop(dropper, **kwargs)
at_drop(dropper, **kwargs) - called when this object has been dropped.
at_pre_say(speaker, message, **kwargs)
at_say(message, msg_self=None, msg_location=None, receivers=None, msg_receivers=None, **kwargs)
at_look(target, **kwargs)
at_desc(looker=None)
"""
def global_search(self, searchdata):
"""
Search for something globally.
"""
return super().search(searchdata, global_search=True)
def has(self, item):
"""
Return true if object has an item.
Where item is probably a string name to match an item's key.
It can also be a type, for instance:
character.has(typeclasses.drinkables.TeaCup)
"""
for i in self.contents:
if isinstance(item, str) and (i.key == item or i.aliases.get(item)):
return i
if item is type(i):
return i
if i == item:
return i
return None
def characters_here(self, puppets=False):
"""
Return a list of characters in the current location.
"""
return [char
for char in self.location.contents
if char != self and
(char.is_typeclass("typeclasses.characters.Character")
or (char.is_typeclass("typeclasses.puppets.Puppet") and puppets))]
def delay_sequence(self, sequence_str, time_delay=1, *args):
"""Run a sequence of messages or commands with a delay.
The 'sequence_str' is a number of commands separated by ';;'
character sequence. The command can by standard things like
'say' or 'give', but can be a message delivered to the room
location, if it begins with a # or 'gm'.
The command can also be a number, in which case, the next
command (and all subsequent commands), will be delayed by that
number of seconds.
For instance:
'say Hello there, want a drink? ;; 8 ;; # He works on shaking a cocktail. ;; 2 ;; shake whisky = avatar'
Which could show:
Blonde elf says, "Hello there, want a drink?"
<8 seconds pass>
He works on shaking a cocktail.
<2 seconds pass>
You now have a whisky.
"""
def convert(x):
try:
return int(x)
except ValueError:
return x
if self.db.current_sequence and self.db.current_sequence == sequence_str:
logger.info("Duplicate sequences. Ignoring.")
return
self.db.current_sequence = sequence_str
lines = [convert(line) for line in split(r" *;; *", sequence_str)]
pause = 0
for line in lines:
if isinstance(line, int):
time_delay = line
else:
pause = pause + time_delay
m = match(r"(^# *|^gm +)(.*)", line)
if m:
logger.info(f"GM'd: {m.group(2)}")
delay(pause, self.location.msg_contents, routput(m.group(2), *args))
else:
cmd = routput(line, *args)
delay(pause, self.do_cmd, cmd)
delay(pause, self.attributes.remove, "current_sequence")
def at_post_move(self, source_location, move_type="move", **kwargs):
"""
Delete ourselves if we are living inside a pet.
"""
super().at_post_move(source_location, move_type)
if self.location.is_typeclass("typeclasses.pets.Pet"):
delay(5, self.delete)
def at_give(self, giver, getter, **kwargs):
"""
Call a puppet's 'other_given' method, if defined.
"""
super().at_give(giver, getter)
if hasattr(getter, 'other_given') and callable(getter.other_given):
getter.other_given(giver, self)
class Listener:
"""
Mix in class with methods for being able to respond
to events from characters.
"""
def get_character_label(self, character):
cleaned = sub(r"|[A-z/]", "",
character.get_display_name(self))
return cleaned.split(' ')[-1]
def trigger_sequence(self, seq, character, *other_stuff):
"""
The actions associated with the trigger
{0} - character's real name
{1} - character's sdesc (from puppet's pov)
{2} - character's label, last word in sdesc
{3} - character object
"""
logger.info(f"Triggering {seq}")
self.delay_sequence(seq, 1, character.key,
character.get_display_name(self),
self.get_character_label(character),
character, *other_stuff)
def get_attribute_trigger(self, label, character):
"""
Return the attribute where 'key' is the label, and the
category can by either the 'character' or its display name.
"""
name = self.get_character_label(character)
return (self.attributes.get(key=label, category=character.key) or
self.attributes.get(key=label, category=name) or
self.attributes.get(key=label))
def move_triggers(self, label, character):
"""
Return a list of triggers matching 'label' and 'character'.
"""
seq = self.get_attribute_trigger(label, character)
if seq:
self.trigger_sequence(seq, character)
def other_arrive(self, character):
"""
Execute a command when a character arrives in the same location.
"""
self.move_triggers('arrive', character)
def other_leave(self, character):
"""
Execute a command when a character arrives in the same location.
"""
self.move_triggers('leave', character)
def say_triggers(self, label, character, speech):
trigs = self.get_attribute_trigger(label, character)
if trigs:
try:
for _, trigger in enumerate(dict(trigs)):
seq = trigs[trigger]
if match(trigger, speech, IGNORECASE):
self.trigger_sequence(seq, character)
except:
self.trigger_sequence(trigs, character)
def other_say(self, speaker, speech):
self.say_triggers('say', speaker, speech)
def other_sayto(self, speaker, speech):
self.say_triggers('sayto', speaker, speech)
def other_given(self, giver, obj):
target = giver.get_display_name(self).split(' ')[-1]
self.execute_cmd(f"emote /Me says to /{target}, \"Thanks for the {obj.name}.\"")
def do_cmd(self, cmd):
"""
Like 'execute_cmd', but for objects.
"""
logger.info(f"Executing: {cmd}")
m = match(r"teleport +(.*?) *= *(.*)", cmd)
if m:
o = self.global_search(m.group(1))
d = self.global_search(m.group(2))
logger.info(f"Teleporting: {m.group(1)} {o} to {d}")
if o and d:
o.move_to(d, quiet=True)
return
m = match(r"tag_all +(.*) *", cmd)
if m:
tag = m.group(1)
logger.info(f"Tagging '{tag}'")
for c in self.characters_here(puppets=True):
# Two tags?
c.tags.add(tag)
c.tags.add(tag, category="mp")
return
m = match(r"gift_all ([A-z]+)( *: *([^:]+)( *: *(.*))?)?", cmd)
if m:
logger.info(f"Higher Gift: {m.group(1)}")
for c in self.characters_here(puppets=True):
self.do_gift(c, m.group(1), m.group(3), m.group(5))
return
m = match(r"gift ([A-z]+) *?( to|=)? *([^:]+)( *: *([^:]+)( *: *(.*))?)?", cmd)
if m:
logger.info(f"Higher Gift: {m.group(1)} to {m.group(3)}")
self.do_gift(m.group(3), m.group(1), m.group(5), m.group(7))
return
if self.is_typeclass("typeclasses.characters.Character"):
self.execute_cmd(cmd)
else:
logger.info(f"Can't do {cmd}, yet.")
def cleanup(self):
"""
Execute orders.
@set thing/objects_here = other_object
@set thing/objects_here = [one_object, two_object]
"""
def move_to_me(obj):
o = self.global_search(obj)
if o:
o.move_to(self, quiet=True)
objs = self.db.objects_here
if objs:
if isinstance(objs, str):
move_to_me(objs)
else:
for obj in objs:
move_to_me(obj)