389 lines
14 KiB
Python
389 lines
14 KiB
Python
"""
|
|
Characters
|
|
|
|
Characters are (by default) Objects setup to be puppeted by Accounts.
|
|
They are what you "see" in game. The Character class in this module
|
|
is setup to be the "default" character type created by the default
|
|
creation commands.
|
|
|
|
"""
|
|
|
|
from datetime import datetime, timedelta
|
|
from re import match, compile, sub
|
|
|
|
from evennia.contrib.game_systems.gendersub import GenderCharacter
|
|
from evennia.contrib.rpg.rpsystem import ContribRPCharacter
|
|
from evennia.contrib.rpg.rpsystem import send_emote
|
|
from evennia.prototypes.spawner import spawn
|
|
from evennia.utils import delay, logger
|
|
|
|
from utils.word_list import routput, choices
|
|
from .objects import Object
|
|
from .tutorial import TutorBird, TutorialState
|
|
|
|
|
|
INTRO = """
|
|
As the surrounding mists dissipate, you find yourself in an ancient, halcyon forest dripping with moss. You see an envelope of parchment wedged under a scaly protrusion of bark...inside, a letter in familiar penmanship, personally addressed to you, which you pick up.
|
|
|
|
A little gray bird flies by you, almost grazing your ear!"""
|
|
|
|
READ_LETTER = """You read a letter with an oddly familiar penmanship:
|
|
|
|
My dear {0},
|
|
|
|
If you are reading this, you've found the world I was overly excited in relaying to you over drinks in Marsivan. Most excellent. Enjoy this halcyon world, unspoiled and idyllic.
|
|
|
|
I would suggest seeing/doing these:
|
|
|
|
- Jump in a puddle
|
|
- Sit on some moss
|
|
- Feed the beast until it is friendly
|
|
- Catch an obnoxious fish
|
|
- Get and read a book
|
|
- Have some tea and scones
|
|
|
|
Oh, I also suggest checking out |wWyldwood|n, a fabulous bar that doesn't open very often, but is quite fun when it does.
|
|
|
|
I'm here, so join me in a cup of tea and we can reconnect and reminisce of glorious days gone by, and the utter curiosity that surrounds us.
|
|
|
|
Your friend,
|
|
Dabbler
|
|
|
|
(Type 'help start' for details on playing this game)"""
|
|
|
|
|
|
class Character(Object, GenderCharacter, ContribRPCharacter):
|
|
"""
|
|
The Character just re-implements some of the Object's methods and hooks
|
|
to represent a Character entity in-game.
|
|
|
|
See mygame/typeclasses/objects.py for a list of
|
|
properties and methods available on all Object child classes like this.
|
|
"""
|
|
pose = True
|
|
|
|
def at_object_creation(self):
|
|
"called when a character is first created."
|
|
self.db.tutorstate = 0
|
|
|
|
if self.dbref != "#1" and not self.is_typeclass('typeclasses.puppets.Puppet'):
|
|
self.create_letter()
|
|
self.create_ticket()
|
|
|
|
def delete_inv(self, typeclass):
|
|
for obj in self.contents:
|
|
if obj.is_typeclass(typeclass):
|
|
obj.delete()
|
|
|
|
def at_post_puppet(self, **kwargs):
|
|
if self.db.visited:
|
|
self.msg(f"""\n“Welcome back, {self.key.capitalize()}.”\n""")
|
|
if self.location.key == "Wyldwood Bar":
|
|
self.msg("You wake up in a meadow with a strange dream of a bar...")
|
|
self.delete_inv("typeclasses.drinkables.Cocktail")
|
|
meadow = self.search("Frog Meadow", global_search=True, quiet=True)
|
|
if isinstance(meadow, list):
|
|
meadow = meadow[0]
|
|
self.move_to(meadow, quiet=True, use_destination=True)
|
|
self.execute_cmd("look")
|
|
else:
|
|
self.db.visited = True
|
|
self.db.tutorstate = 0
|
|
self.fix_letter()
|
|
TutorBird.do_start_tutorial(self)
|
|
self.msg(INTRO)
|
|
self.account.db._last_puppet = self
|
|
|
|
def at_pre_unpuppet(self, **kwargs):
|
|
"""
|
|
Make sure we aren't left sitting down when logging out.
|
|
"""
|
|
if self.db.is_sitting:
|
|
chair = self.db.is_sitting
|
|
chair.db.sitter = None
|
|
self.db.is_sitting = None
|
|
|
|
def msg(self, text=None, from_obj=None, session=None, **kwargs):
|
|
"""
|
|
Capitalizes messages sent to the user.
|
|
This just looks better to me.
|
|
"""
|
|
def capitalize_line(line):
|
|
if not line or line.strip() == "":
|
|
return line
|
|
elif match(r"^\|[bm][a-z].*", line):
|
|
return "The " + line
|
|
elif match(r"^\|[A-z][a-z].*", line):
|
|
return line[0:1] + line[2].upper() + line[3:]
|
|
else:
|
|
return line[0].upper() + line[1:]
|
|
|
|
if text and isinstance(text, str) and len(text) > 0:
|
|
text = capitalize_line(text)
|
|
elif text and isinstance(text, tuple):
|
|
text = (capitalize_line(text[0]), text[1])
|
|
super().msg(text, from_obj=from_obj, session=session, **kwargs)
|
|
|
|
def create_letter(self):
|
|
"create a welcome letter in a character's inventory"
|
|
letter = spawn({
|
|
"typeclass": "typeclasses.readables.Letter",
|
|
"key": "letter",
|
|
"desc": "An envelope with a letter of familiar penmanship.",
|
|
})[0]
|
|
letter.db.inside = READ_LETTER.format(self.name.capitalize())
|
|
letter.location = self
|
|
|
|
def fix_letter(self):
|
|
letter = self.search('letter', location=self)
|
|
if letter:
|
|
text = sub(r"My dear [^,]*", f"My dear {self.name}", letter.db.inside)
|
|
letter.db.inside = text
|
|
|
|
def create_ticket(self):
|
|
"""
|
|
Create a dated ticket.
|
|
"""
|
|
today = datetime.now()
|
|
days_ahead = (1 - today.weekday() + 7) % 7 # 1 is Tuesday
|
|
next_tuesday_date = today + timedelta(days=days_ahead)
|
|
tuesday = next_tuesday_date.date()
|
|
|
|
day = tuesday.day
|
|
month = tuesday.strftime("%B") # Full month name
|
|
if 10 <= day % 100 <= 20:
|
|
suffix = 'th'
|
|
else:
|
|
suffix = {1: 'st', 2: 'nd', 3: 'rd'}.get(day % 10, 'th')
|
|
|
|
ticket = spawn({
|
|
"typeclass": "typeclasses.readables.Letter",
|
|
"key": "ticket",
|
|
"desc": "A ticket to |wWyldwood Bar|n.",
|
|
})[0]
|
|
ticket.db.inside = f"""
|
|
The ticket is dated, {day}{suffix} of {month} ...
|
|
It also reads that the portal opens in the field at seven.
|
|
"""
|
|
ticket.location = self
|
|
|
|
def get_display_things(self, looker, *args, **kwargs):
|
|
return super().get_display_things(looker, pose=False)
|
|
|
|
def do_take(self, to_take, from_whom):
|
|
"""
|
|
A character has a _steal_command. What are the limitations?
|
|
"""
|
|
victim = self.search(from_whom)
|
|
if victim:
|
|
thing = victim.has(to_take)
|
|
if thing and thing.db.can_take:
|
|
self.msg(f"You take {thing.key} from {victim.key}.")
|
|
thing.move_to(self, quiet=True, use_destination=True)
|
|
return
|
|
|
|
self.msg(f"{victim.key} doesn't have a {to_take} you can take.")
|
|
|
|
def pronoun_subjective(self, uppercase=False):
|
|
gender = self.attributes.get('gender')
|
|
if gender == "male":
|
|
results = 'He'
|
|
elif gender == "female":
|
|
results = 'She'
|
|
elif gender == "neutral":
|
|
results = 'It'
|
|
else:
|
|
results = 'They'
|
|
|
|
return results if uppercase else results.lower()
|
|
|
|
def gendered_text(self, text):
|
|
"""
|
|
Replace entries, like |s and |p with pronouns.
|
|
Like 'he' and 'her'
|
|
"""
|
|
gender_rx = compile(r"(?<!\|)\|(?!\|)[sSoOpPaA]")
|
|
return gender_rx.sub(lambda x: self._get_pronoun(x), text)
|
|
|
|
def return_appearance(self, looker, **kwargs):
|
|
"""
|
|
Can be overridden or appended with an effect.
|
|
Does the looker affect this?
|
|
"""
|
|
# To replace, temporarily, a description:
|
|
# @set woman/temp_desc:effect = "They fade away from view."
|
|
new_desc = self.attributes.get(category="effect",
|
|
key="temp_desc")
|
|
if new_desc:
|
|
return self.gendered_text(new_desc)
|
|
|
|
pre_desc = self.attributes.get(category="effect",
|
|
key="pre_desc") or ""
|
|
post_desc = self.attributes.get(category="effect",
|
|
key="post_desc") or ""
|
|
reg_desc = super().return_appearance(looker)
|
|
|
|
# pronoun = self.pronoun_subjective(True)
|
|
return self.gendered_text(pre_desc +
|
|
reg_desc.replace('|wYou see:|n',
|
|
'|S has') +
|
|
post_desc)
|
|
|
|
def at_pre_move(self, destination, *args, **kwargs):
|
|
"""
|
|
Called by self.move_to when trying to move somewhere. If this returns
|
|
False, the move is immediately canceled.
|
|
"""
|
|
self.db.tutorstate = (self.db.tutorstate or 0) | TutorialState.MOVE.value
|
|
|
|
if self.db.is_sitting:
|
|
self.msg("You stand up first...")
|
|
self.db.is_sitting = False
|
|
|
|
# @lock thing = tethered:id(#19)
|
|
# @set thing/tethered_msg = "Let's put that back"
|
|
for thing in self.contents:
|
|
to = thing.locks.get('tethered')
|
|
if to:
|
|
m = match(r".*:id\((.*)\)", to)
|
|
if m:
|
|
location = m.group(1)
|
|
dest = self.search(location)
|
|
msg = thing.db.tethered_msg
|
|
if dest and msg:
|
|
thing.location = dest
|
|
self.msg(msg)
|
|
else:
|
|
logger.warn(f"Found tethered, {thing} to #{location}, but dest is {dest} and msg is '{msg}'. Set both.")
|
|
else:
|
|
logger.warn(f"Found tethered, {thing} to #{to}, but that needs to be 'tethered:id(num) where num is the ID# of an object/place.")
|
|
|
|
for puppet in self.puppets_here():
|
|
puppet.other_leave(self)
|
|
|
|
return super().at_pre_move(destination)
|
|
|
|
def at_pre_say(self, message, **kwargs):
|
|
"While we could/should do 'at_say', this should be easier."
|
|
self.db.tutorstate = (self.db.tutorstate or 0) | TutorialState.SAY.value
|
|
return super().at_pre_say(message)
|
|
|
|
def at_look(self, target, **kwargs):
|
|
"""
|
|
When we look at something that _might_ be hidden, we check
|
|
if we are looking at something that _is_ hidden, i.e. has a
|
|
view lock of a particular tag, and if so, we interpret this as
|
|
'you are looking at something by name', therefore you see it.
|
|
So we give ourselves the right to see it from now on.
|
|
|
|
To use this, simply add the following lock:
|
|
|
|
@set thing/hidden_tag = target
|
|
@lock thing = view:tag(hidden_target)
|
|
|
|
Where thing is the single name of the hidden object.
|
|
And 'target' is some label. If not given, this defaults
|
|
to the name of the thing.
|
|
"""
|
|
|
|
# If they 'look' at a target with tags, they get those locks:
|
|
# For instance: @set waterfall/hidden_tag = "hidden_cave"
|
|
if target.db.hidden_tag:
|
|
for hidden_tag in target.db.hidden_tag.split(';'):
|
|
self.tags.add(hidden_tag)
|
|
|
|
if target.is_typeclass("typeclasses.rooms.Room"):
|
|
self.db.tutorstate = (self.db.tutorstate or 0) | TutorialState.LOOK.value
|
|
else:
|
|
self.db.tutorstate = (self.db.tutorstate or 0) | TutorialState.LOOKAT.value
|
|
|
|
# Regardless of what happened before, we return the normal
|
|
# function call.
|
|
return super().at_look(target)
|
|
|
|
def announce_action(self, message, exclude=None):
|
|
"""
|
|
Replaces a location's 'msg_contents' with an emote.
|
|
"""
|
|
# All this does is add the character's gender to the message.
|
|
# So $pron(you,op) will render "you" or "him"
|
|
newmsg = sub(r"\$pron\((.*?),(.*?)\)", f"$pron(\\1,\\2 {self.db.gender})", message)
|
|
# While $pron(you) will render "you" and "he":
|
|
newmsg = sub(r"\$pron\(([^,]*?)\)", f"$pron(\\1, {self.db.gender})", newmsg)
|
|
choose = choices(newmsg)
|
|
logger.info(choose)
|
|
self.location.msg_contents(f"{choose}", from_obj=self, exclude=exclude)
|
|
|
|
def spell_sequence(self, location, messages, time_delay=1):
|
|
"""
|
|
Send one or more messages to 'location' with a delay.
|
|
"""
|
|
location.msg_contents("\n" + routput(messages[0]))
|
|
for idx, msg in enumerate(messages[1:]):
|
|
delay(time_delay * (idx + 1),
|
|
location.msg_contents, "\n" + routput(msg))
|
|
|
|
def do_fly(self, location):
|
|
"""
|
|
Allow a wizard to arrive thematically into a new location.
|
|
"""
|
|
if location == "home":
|
|
dest = self.home
|
|
else:
|
|
dest = self.global_search(location)
|
|
if dest and dest.is_typeclass("typeclasses.characters.Character"):
|
|
dest = dest.location
|
|
elif not dest or not dest.is_typeclass("typeclasses.rooms.Room"):
|
|
self.msg(f"Not sure where, '{location}' is.")
|
|
return
|
|
|
|
if self.db.disappear_msg:
|
|
self.spell_sequence(self.location,
|
|
self.db.disappear_msg.split(';;'),
|
|
self.db.appear_delay or 2)
|
|
self.move_to(dest, move_type="magic", quiet=True)
|
|
|
|
if self.db.reappear_msg:
|
|
self.spell_sequence(self.location,
|
|
self.db.reappear_msg.split(';;'),
|
|
self.db.appear_delay or 2)
|
|
|
|
def characters_here(self):
|
|
"""
|
|
Return a list of characters in the current location.
|
|
"""
|
|
return [char for char in
|
|
self.search("", typeclass="typeclasses.characters.Character",
|
|
location=self.location, quiet=True)
|
|
if char != self]
|
|
|
|
# Hooks to the puppets:
|
|
|
|
def puppets_here(self):
|
|
"""
|
|
Return a list of puppets in the current location.
|
|
Only used for calling hooks on the animatronic dolls.
|
|
"""
|
|
return [puppet for puppet in
|
|
self.search("", typeclass="typeclasses.puppets.Puppet",
|
|
location=self.location, quiet=True)
|
|
if puppet != self]
|
|
|
|
def at_post_move(self, past_location, move_type="move", **kwargs):
|
|
super().at_post_move(past_location, move_type)
|
|
for puppet in self.puppets_here():
|
|
puppet.other_arrive(self)
|
|
|
|
def deeper_search(self, item_str):
|
|
targets = self.search(item_str, quiet=True)
|
|
if len(targets) > 0:
|
|
return targets
|
|
|
|
for target in self.location.contents:
|
|
logger.info(f"Looking for books at {target}")
|
|
inv_items = self.search(item_str, quiet=True,
|
|
location=target)
|
|
if len(inv_items) > 0:
|
|
return inv_items
|
|
return None
|