moss-n-puddles/commands/wizards.py
Howard Abrams 66cabcc1a2 Animate the Wee Beastie and scrying into rooms
This commit gives an "AI" capability to any object (still
triggered by the 'say' command), so now the Wee Beastie can do more
than purr.

Also fixes the Witch and the Dragon's movements throughout the Realm.

We can also Scry into rooms in order to watch the behavior of the Chatbots.
2026-04-16 17:09:15 -07:00

469 lines
14 KiB
Python
Executable file

#!/usr/bin/env python
from random import choice
from re import match, findall
from evennia import CmdSet, create_script
from evennia.utils import delay, logger
from evennia.commands.default.muxcommand import MuxCommand
from evennia.contrib.rpg.rpsystem import send_emote
from evennia.prototypes.spawner import spawn
from evennia.utils.utils import int2str
from .command import Command
from typeclasses.scripts import DonkeyHeadSpell
from typeclasses.drinkables import Cocktail
from utils.word_list import routput, pluralize
class CmdFly(Command):
"""Cast the 'fly' spell.
Make sure you set the following properties, for instance:
@set self/disappear_msg = "The wizard disappears in a puff of smoke."
@set self/reappear_msg = "A plume of <<white ^ light blue ^ gray ^ >>
smoke appears... ;; When the smoke clears, a
wizard <<emerges ^ materializes>>."
@set self/appear_delay = 3
The last setting is the number of seconds between message segments
(those are separated by double semicolons).
"""
key = "fly"
locks = "cmd:holds()"
def func(self):
"""
Call the 'do_fly' method on the caller.
"""
self.caller.do_fly(self.args.strip())
class CmdMagic(Command):
"""
Cast a generic 'magic' spell.
Usage:
wave [ effect(s) ]
Where 'effects' is a statement of what happens after the magic erupts.
Note that you can have multiple effects separated by two semicolons.
You can tailor the effects to your character with the following:
- |y$you()|n: Replaced by "you" and your name for others, like "old gnome"
- |y$your()|n: Replaced by "your" and your possessive name for others, like "old gnome's"
- |y$conj(verb)|n: Replaced by "verb" for you, and "verbs" for others.
- |y$pron(you)|n: Replaced by "you" for you, but "he" or "she" for others.
- |y$pron(your)|n: Replaced by "your" for you, but "his" or "her" for others.
While flexible, you would use this complex replacement in either
|gnicks|n or as part of a standard magical prefix, by setting the
property:
@set self/magic_msg = "$You() $conj(shake) $pron(your) necklace of bones!"
"""
key = "magic"
aliases = ["wave"]
locks = "cmd:holds()"
def func(self):
"""
Call the 'do_magic' method on the caller.
"""
wizard = self.caller
msgs = wizard.db.magic_msg
if msgs:
msgs = msgs.split(';;')
else:
msgs = [
"$You() $conj(wave) $pron(your) " + self.obj.key + ".",
"<< Sparks ^ Colored lights ^ Flashes ^ Flashes >> of |yoctarine|n << appear ^ emerge ^ materialize >> as << the ^ >> magic << coalesces into an amorphous show of power ^ blends into swirling patterns ^ weaves together>>."
]
if self.args:
msgs = msgs + self.args.strip().split(';;')
wizard.spell_sequence(None, msgs, wizard.db.magic_delay or 3)
class CmdMakeItem(Command):
"""
Create one or more items from thin air.
Usage:
make [ number ] name [ description ]
Where 'effects' is a statement of what happens after the magic erupts.
Note that you can have multiple effects separated by two semicolons.
You can tailor the effects to your character with the following:
- |y$you()|n: Replaced by "you" and your name for others, like "old gnome"
- |y$your()|n: Replaced by "your" and your possessive name for others, like "old gnome's"
- |y$conj(verb)|n: Replaced by "verb" for you, and "verbs" for others.
- |y$pron(you)|n: Replaced by "you" for you, but "he" or "she" for others.
- |y$pron(your)|n: Replaced by "your" for you, but "his" or "her" for others.
While flexible, you would use this complex replacement in either
|gnicks|n or as part of a standard magical prefix, by setting the
property:
@set self/magic_msg = "$You() $conj(shake) $pron(your) necklace of bones!"
"""
key = "make"
locks = "cmd:holds()"
def parse(self):
"""
Allows the following phrases:
candy
2 candy Piece of Turkish Delight.
candy Piece of Turkish Delight.
"turkish delight" Piece of Turkish Delight.
4 "turkish delight" Piece of Turkish Delight.
"""
pattern = r'"(.*?)"|(\S+)'
matches = findall(pattern, self.args.strip())
try:
self.item_number = int(matches[0][0] or matches[0][1])
self.item_name = matches[1][0] or matches[1][1]
start = 2
except ValueError:
self.item_number = 1
self.item_name = matches[0][0] or matches[0][1]
start = 1
words = [m[0] or m[1] for m in matches[start:]]
self.item_desc = " ".join(words)
def func(self):
"""
Call the 'do_magic' method on the caller.
"""
wizard = self.caller
msgs = wizard.db.make_msg or wizard.db.magic_msg
if msgs:
msgs = msgs.split(';;')
else:
msgs = [
"$You() $conj(snap) $pron(your) fingers.",
]
if self.item_number == 1:
if match(r"^[aeiou]", self.item_name):
name = f"an {self.item_name}"
else:
name = f"a {self.item_name}"
else:
name = int2str(self.item_number) + " " + \
pluralize(self.item_name)
# from evennia.utils.utils import int2str ; print(int2str("2"))
msgs = msgs + [ name + " appear in $pron(your) hand." ]
wizard.spell_sequence(None, msgs, wizard.db.magic_delay or 3)
# FIXME So that the cat will eat it and it can't be littered:
if not self.item_desc or self.item_desc == "":
self.item_desc = f"Conjured by {wizard.get_name()}."
for _ in range(self.item_number):
item = spawn({
"typeclass": "typeclasses.consumables.Litterable",
"key": self.item_name,
"desc": self.item_desc
})[0]
item.location = wizard
class CmdSetWand(CmdSet):
"""
All wizard spells are tied to a 'wand' that might be flavored.
"""
def at_cmdset_creation(self):
super().at_cmdset_creation()
self.add(CmdFly)
self.add(CmdMagic)
self.add(CmdMakeItem)
class CmdScry(Command):
"""Cast the 'scry' spell to view a room."""
key = "scry"
locks = "cmd:holds()"
def func(self):
"""
Call the 'do_show_room' method on the object.
"""
self.obj.do_show_room(self.caller, self.args.strip())
class CmdSetScry(CmdSet):
"""
The set containing the 'scry' command.
"""
def at_cmdset_creation(self):
super().at_cmdset_creation()
self.add(CmdScry)
class CmdMakeCocktail(MuxCommand):
"""
For the 'Bartender' especially.
Usage:
shake |wcocktail|n = |wpatron|n
If patron is not given or not found, the drink will be in your
inventory, and you can call |ggive|n to pass it along.
If cocktail name isn't given (or matches anything), a random
one will be created.
"""
key = 'shake'
locks = "cmd:perm(gm) or perm(Admin)"
def func(self):
dest = self.caller
if self.rhs:
dest = self.caller.search(self.rhs)
Cocktail.make(dest, self.caller, self.lhs)
class CmdGift(MuxCommand):
"""
Give a special gift to a character.
Usage:
gift <gift> to <char> [ : name : desc ]
"""
key = "gift"
locks = "cmd:perm(gm) or perm(Admin)"
def func(self):
m = match(r"([A-z]+) *?( to|=)? *(.+)( *: *[A-z]+( *: *[A-z]+)?)?",
self.args.strip())
if m:
# logger.info(f"Gift: {m.group(1)} to {m.group(3)}")
self.caller.do_gift(m.group(3), m.group(1), m.group(4), m.group(5))
else:
self.caller.msg("Usage: gift <gift> to <char> [ : name : desc ]")
class CmdGM(MuxCommand):
"""
The gm command allows anything to be emoted into a room.
Usage:
gm A bat flies into the room!
gm/gnome You hear a distant ringing
"""
key = "gm"
aliases = ["#"]
locks = "cmd:perm(gm) or perm(Admin)"
def func(self):
from typeclasses.things import Scribe
send_to = []
for switch in self.switches:
o = self.caller.search(switch, global_search=True)
if o:
send_to = send_to + [o]
if not send_to:
send_to = [self.caller.location]
me = self.caller
msg = routput(self.args)
for o in send_to:
if o.is_typeclass('typeclasses.rooms.Room'):
# Send the message to all characters and any recording
# scribes in attendance:
chars = o.contents_get(None, 'character') + [o.has(Scribe)]
send_emote(me, chars, msg, 'say', None)
elif o.is_typeclass('typeclasses.characters.Character'):
o.msg(msg)
class CmdSpell(Command):
"""
Cast one of the few spells we've created that affect others.
Usage:
spell donkey on lizardman
"""
key = "spell"
aliases = ['cast']
locks = "cmd:perm(gm) or perm(Admin)"
def parse(self):
self.spell = None
self.target = None
m = match(r"([^ ]+)( +on +(.+))?", self.args.strip())
if m:
self.spell = m.group(1)
self.target = m.group(3)
def func(self):
caster = self.caller
if not self.spell:
caster.msg('Usage: cast <spell> [on <target>]')
return
char = None
if self.target:
char = caster.search(self.target)
if not char:
return
if self.spell == 'donkey' and char:
create_script(key="donkey_head",
typeclass=DonkeyHeadSpell,
interval=130,
start_delay=True,
attributes=[("target", char)])
caster.msg(f"You cast |wHead of Donkey|n on {char}")
else:
caster.msg(f"You fail to cast {self.spell}")
class CmdGMTrigger(Command):
"""The trigger command kicks off a series of named events.
Usage:
trigger trigger-name
trigger :game trigger-name
trigger/character trigger-name
trigger/character:game trigger-name
Where 'game' defaults to the value previously set:
@set npc/currentgame = "session1"
Triggers are typically set on the NPC (which would be in the room
with the PCs) or the room. Using the command:
@set npc/triggers:session1 = {"darkness": {"desc": "Make the
room go black", "timer": 1, "events": [ "The room gets dark",
"And then pitch-black.", ("You can't help it, but scream!", "You
hear a scream!")]}}
The 'set' command, as a complicated data structure, should be set
in a batchcommand.
"""
key = "trigger"
aliases = ["trig"]
locks = "cmd:perm(gm) or perm(Admin)"
def parse(self):
m = match(r" *(/[^: ]+)?(:[^ ]+)? *(.*)", self.args)
if m:
self.name = m.group(3)
if m.group(2):
self.game = m.group(2)[1:]
else:
self.game = None
if m.group(1):
self.switches = m.group(1)[1:].split(',')
else:
self.switches = []
else:
self.caller.msg("Usage trigger/dest:game trigger")
return False
def func(self):
npc = self.caller
self.send_to = []
for switch in self.switches:
o = npc.search(switch)
if o:
self.send_to = self.send_to + [o]
else:
return
game = self.game or npc.db.currentgame
if not game:
npc.msg("Specify the game, or set a default with |g@set self/currentgame = <game>")
return
triggers = dict(npc.attributes.get(key='triggers', category=game))
if not self.name:
npc.msg("What event do you want to trigger?")
for name, details in triggers.items():
npc.msg(f" - {name} : {details['desc']}")
return
trigger = triggers[self.name]
if trigger:
npc.msg(f"Triggering: |w{trigger['desc']}")
self.trigger(trigger['events'],
trigger.get('timer', 5),
self.send_to)
else:
npc.msg(f"Didn't find '{self.name}' to trigger.")
def trigger(self, events, time_delay, dests=[]):
"""
Given a list of events, send each events at an interval.
If an event is a tuple instead of a string, the first
element goes to 'dests' and the second goes to the room
(based on the caller's location).
"""
target = None
if len(self.send_to) > 0:
target = self.send_to[0]
else:
targets = self.caller.characters_here()
if targets:
target = choice(targets)
if target:
name = target.db._sdesc or target.key
else:
name = ''
for idx, event in enumerate(events):
if isinstance(event, str):
if event.startswith("@"):
self.caller.execute_cmd(event[1:])
else:
if target:
msg = target.gendered_text(routput(event, name))
else:
msg = routput(event)
delay(time_delay * idx,
self.caller.location.msg_contents,
"\n" + msg)
else:
char = target.gendered_text(routput(event[0], name))
room = target.gendered_text(routput(event[1], name))
if room:
delay(time_delay * idx,
self.caller.location.msg_contents,
"\n" + routput(room),
exclude=dests)
if char:
for dest in dests:
delay(time_delay * idx,
dest.msg,
"\n" + routput(char))