moss-n-puddles/typeclasses/scripts.py
2026-04-22 12:09:29 -07:00

313 lines
12 KiB
Python

"""
Scripts
Scripts are powerful jacks-of-all-trades. They have no in-game
existence and can be used to represent persistent game systems in some
circumstances. Scripts can also have a time component that allows them
to "fire" regularly or a limited number of times.
There is generally no "tree" of Scripts inheriting from each other.
Rather, each script tends to inherit from the base Script class and
just overloads its hooks to have it perform its function.
"""
from enum import Enum
from itertools import cycle
from pathlib import Path
from random import choice
from evennia.scripts.scripts import DefaultScript
from evennia.prototypes.spawner import spawn
from evennia.utils import logger, delay
from evennia.utils.search import search_object
from typeclasses.characters import Character
class Script(DefaultScript):
"""
This is the base TypeClass for all Scripts. Scripts describe
all entities/systems without a physical existence in the game world
that require database storage (like an economic system or
combat tracker). They
can also have a timer/ticker component.
A script type is customized by redefining some or all of its hook
methods and variables.
* available properties (check docs for full listing, this could be
outdated).
key (string) - name of object
name (string)- same as key
aliases (list of strings) - aliases to the object. Will be saved
to database as AliasDB entries but returned as strings.
dbref (int, read-only) - unique #id-number. Also "id" can be used.
date_created (string) - time stamp of object creation
permissions (list of strings) - list of permission strings
desc (string) - optional description of script, shown in listings
obj (Object) - optional object that this script is connected to
and acts on (set automatically by obj.scripts.add())
interval (int) - how often script should run, in seconds. <0 turns
off ticker
start_delay (bool) - if the script should start repeating right away or
wait self.interval seconds
repeats (int) - how many times the script should repeat before
stopping. 0 means infinite repeats
persistent (bool) - if script should survive a server shutdown or not
is_active (bool) - if script is currently running
* Handlers
locks - lock-handler: use locks.add() to add new lock strings
db - attribute-handler: store/retrieve database attributes on this
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
create(key, **kwargs)
start() - start script (this usually happens automatically at creation
and obj.script.add() etc)
stop() - stop script, and delete it
pause() - put the script on hold, until unpause() is called. If script
is persistent, the pause state will survive a shutdown.
unpause() - restart a previously paused script. The script will continue
from the paused timer (but at_start() will be called).
time_until_next_repeat() - if a timed script (interval>0), returns time
until next tick
* Hook methods (should also include self as the first argument):
at_script_creation() - called only once, when an object of this
class is first created.
is_valid() - is called to check if the script is valid to be running
at the current time. If is_valid() returns False, the running
script is stopped and removed from the game. You can use this
to check state changes (i.e. an script tracking some combat
stats at regular intervals is only valid to run while there is
actual combat going on).
at_start() - Called every time the script is started, which for persistent
scripts is at least once every server start. Note that this is
unaffected by self.delay_start, which only delays the first
call to at_repeat().
at_repeat() - Called every self.interval seconds. It will be called
immediately upon launch unless self.delay_start is True, which
will delay the first call of this method by self.interval
seconds. If self.interval==0, this method will never
be called.
at_pause()
at_stop() - Called as the script object is stopped and is about to be
removed from the game, e.g. because is_valid() returned False.
at_script_delete()
at_server_reload() - Called when server reloads. Can be used to
save temporary variables you want should survive a reload.
at_server_shutdown() - called at a full server shutdown.
at_server_start()
"""
class KnockScript(Script):
"""
A script to wake the dead.
"""
def at_start(self, **kwargs):
knocker = self.attributes.get("knocker")
if knocker:
room = search_object(self.attributes.get("room")).first()
if room:
room.msg_contents("Someone is knocking on the door...")
gnome = search_object("Dabbler").first()
if gnome:
delay(3, gnome.msg,
f"With your seer stone, you see the knocker is {knocker.key}, the {knocker.db._sdesc}.")
def at_repeat(self, **kwargs):
waker = self.attributes.get("waker")
if waker:
waker.knocked_timed_out()
self.delete()
class CreateSticks(Script):
"""
Script to create sticks.
"""
def at_repeat(self, **kwargs):
woods = self.attributes.get("destination")
results = woods.search('stick')
if results and len(results) > 0 and results[0].location == woods:
pass
else:
stick = spawn({
"typeclass": "typeclasses.things.Stick",
"key": "stick",
"desc": """Its brown and sticky.|/|/Well, by sticky, we mean, wizardly-sticky...an absolutely amazing looking stick...definitely a wizardly stick.""",
})[0]
stick.location = woods
woods.msg_contents("A stick falls from one of the trees and lands on the ground near your feet.")
class CreateHorns(Script):
"""
Script to create calling horns.
"""
def at_repeat(self, **kwargs):
hut = self.attributes.get("destination")
results = hut.search('horn', location=hut, quiet=True)
if len(results) < 1:
horn = spawn({
"typeclass": "typeclasses.sailing.CallingHorn",
"key": "horn",
"desc": "While physical, this curved horn seems to be made from sea mist, as it has an amorphous quality. Wonder what would happen if you |gblow|n this horn?",
})[0]
horn.location = hut
hut.msg_contents("The misty smell of brine wafts in through a window. The mists congeal to form a horn, hanging on a hook near the window.")
class Spell(Script):
"""
A script to clean up the effects of a spell.
"""
def at_stop(self, **kwargs):
target = self.attributes.get("target")
target.attributes.clear(category="effect")
self.delete()
class DonkeyHeadSpell(Spell):
def at_start(self, **kwargs):
target = self.attributes.get("target")
target.attributes.add(
"donkied",
"Heehaw! ;; Heehaw, heehaw!",
category="effect")
target.attributes.add(
"post_desc",
"\nOh, and |s has a donkey's head!",
category="effect")
target.msg("You suddenly feel quite peculiar.")
def at_repeat(self, **kwargs):
self.stop()
class Muttering(Script):
"""
Script to have an objects 'emote' phrases in sequence.
First add settings to the 'npc', for instance:
@set npc/muttering_interval = 120 # for 2 minutes
@set npc/muttering_gap = 5 # for 5 seconds per sequence
@set npc/muttering_formats = [
"sings to |oself as if no one is listening, \"{0}\"",
"continues to sing to |oself, \"{0}\"",
"croons to |oself, \"{0}\"",
"finishes |p verse, \"{0}\"|/",
]
@set npc/muttering_file = "song-lyrics.txt"
Then start the script by running the following:
@script npc = typeclasses.scripts.Muttering
"""
def at_script_creation(self):
self.key = "muttering"
self.desc = "NPCs that Mutter"
self.interval = self.obj.db.muttering_interval or 300 # seconds
self.start_delay = False
self.persistent = True
self.reload()
def reload(self):
self.db.mutter_delay_gap = self.obj.db.muttering_gap or 7
self.db.mutter_sequence_index = 0
mutter_file = self.obj.db.muttering_file
if mutter_file:
logger.info(f"Reading muttering file, {mutter_file}")
self.db.mutters = self.load_mutter(mutter_file)
def at_repeat(self, **kwargs):
"""
Time to mutter something...
"""
self.mutter_sequence()
def mutter_sequence(self):
# if not hasattr(self, 'mutter_sequence_index'):
# self.db.mutter_sequence_index = 0
if self.db.mutter_sequence_index < len(self.db.mutters):
sequences = self.db.mutters[self.db.mutter_sequence_index]
self.db.mutter_sequence_index += 1
else:
sequences = self.db.mutters[0]
self.db.mutter_sequence_index = 1
formats = self.refresh_mutter_formats()
zip_list = zip(sequences, cycle(formats)) \
if len(sequences) > len(formats) \
else zip(sequences, formats[:len(sequences)])
for idx, tup in enumerate(zip_list):
# logger.info(f"delay({idx} * {self.db.mutter_delay_gap}, self.mutter, {tup[0]}, {tup[1]})")
delay(idx * self.db.mutter_delay_gap,
self.mutter, tup[0], tup[1])
def mutter(self, phrase, msg_format):
"""
Mutter something aloud to the room the NPC resides.
Why wait for input from other characters when the NPC can
pretend to be real.
"""
thing = self.obj
room = thing.location
name = choice([
thing.key,
thing.db._sdesc or thing.name,
thing.key.split(" ")[-1]
])
if thing.db.article:
prefix = f"{thing.db.article} {name} {msg_format}"
else:
prefix = f"{name} {msg_format}"
if phrase:
room.msg_contents(
prefix.format(phrase),
from_obj=self.obj)
def refresh_mutter_formats(self):
return self.obj.db.muttering_formats or [
"mutters as if no one is listening, \"{0}\"",
"grumbles to |oself, \"{0}\"",
"continues to mutter to |oself, \"{0}\"",
"murmurs to |oself, \"{0}\"",
"mutters to |oself, \"{0}\"|/",
]
def load_mutter(self, data_file):
"""
Return 'data_file' and return a list of lists.
Where a blank line in the data file separates the list of
lines from others.
"""
path = Path(__file__).with_name(data_file)
mutters = []
with open(path) as file:
curr_mutter = []
for line in file:
if line.strip() == "":
mutters = mutters + [curr_mutter]
curr_mutter = []
else:
curr_mutter.append(line.strip())
return mutters