moss-n-puddles/typeclasses/alchemy.py
Howard Abrams 43a1aefb49 Add a Scoring system to character
As character do things, they can now get a "score" for everything I
think could be interesting/challenging.
2025-08-24 22:34:11 -07:00

398 lines
14 KiB
Python
Executable file
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

#!/usr/bin/env python
from collections import defaultdict
from random import choice, randint, random
from evennia import CmdSet
from evennia.commands.default.muxcommand import MuxCommand
from evennia.prototypes.spawner import spawn
from evennia.utils import delay, iter_to_str, logger
from commands.command import Command
from typeclasses.objects import Object
from typeclasses.scripts import Script
from typeclasses.drinkables import Container
from utils.scoring import Scores
class CmdEmpty(Command):
"""
Empty the cauldron.
Usage:
empty [ cauldron ]
Empty the cauldron before you start working on making a potion.
This destroys either the ingredients or the potion that it contains.
Next, you can start |gadd|ning the potion's components.
"""
key = "empty"
def func(self):
self.obj.do_empty(self.caller)
class CmdAdd(Command):
"""
Add an item from your inventory to the cauldron.
Usage:
add <item> [ to cauldron ]
The item can be a single item you picked up on your travels, like
a bunch of tickleweed, or can be the contents of a container, like
a bottle or teacup.
Keep in mind that potions may require |wenough|n of an item.
For instance, a bottle can contain more liquid than a teacup.
(You can get an empty bottle from the shelf.)
Once all ingredients have been added, you can |gcreate|n a potion.
"""
priority = 2
key = "add"
aliases = "give"
def func(self):
maker = self.caller
item_str = self.args.strip().split(" to ")
if len(item_str) == 0:
maker.msg(f"What do you want to add to the {self.obj.name}?")
return
item = maker.search(item_str[0], location=maker)
if item:
self.obj.do_add(self.caller, item)
class CmdCreate(Command):
"""
Create a potion from the contents of the cauldron.
Usage:
create [ potion ]
After you have |gadd|ned all the ingredients for a potion, cast
this. If you have the correct components, the cauldron will
contain a potion ready for you to |gbottle|n.
"""
key = "create"
aliases = ["make", "brew", "cook"]
def func(self):
self.obj.do_create(self.caller)
class CmdBottle(Command):
"""Bottle the contents of the cauldron.
Usage:
bottle [ potion ]
After you have |gcreate|nd a potion, use this command to put it
into a vial you can consume later (or give to a friend).
If you |gdrop|n the vial, you can always get another potion from
the cauldron (at least, until some one calls |gempty|n).
"""
key = "bottle"
aliases = ["fill", "get vial"]
def func(self):
self.obj.do_bottle(self.caller)
class CmdSetCauldron(CmdSet):
"""
The commands available to people next to a cauldron.
"""
def at_cmdset_creation(self):
self.add(CmdEmpty)
self.add(CmdAdd)
self.add(CmdCreate)
self.add(CmdBottle)
class Cauldron(Object):
"""
A cauldron where we can collect and mix ingredients.
- empty
- add
- mix
- bottle
"""
def at_object_creation(self):
"""
Associate the commands with this instance.
"""
self.cmdset.add_default(CmdSetCauldron)
def return_appearance(self, looker):
"""
Return the full description and contents of this object.
Along with a description of this object, we want to return the
ingredients and items in this object. For that, we duplicate a
lot of already implemented code.
"""
full_desc = self.db.desc
num_items = len(self.contents)
if num_items == 0:
return full_desc + "|/It is empty, and ready for you to |gadd|n ingredients."
# Had to copy the following code from the objects.objects'
# get_display_things so that I could reformat it:
grouped_things = defaultdict(list)
for thing in self.contents:
grouped_things[thing.get_display_name(looker)].append(thing)
thing_names = []
for thingname, thinglist in sorted(grouped_things.items()):
nthings = len(thinglist)
thing = thinglist[0]
singular, plural = thing.get_numbered_name(nthings, looker, key=thingname)
thing_names.append(singular if nthings == 1 else plural)
thing_names = iter_to_str(thing_names)
return full_desc + \
f"|/It contains {thing_names}." if thing_names else ""
def do_empty(self, maker=None):
"""
Delete the contents of the cauldron.
Also randomly return a funny emptying message.
"""
if self.contents == []:
maker.msg(f"The {self.name} is already empty.")
return
msg = choice([
f"The imp <<climbs ^ flies >> down from its perch, <<sniffs ^ tastes ^ samples >> the brew, then <<downs ^ swallows ^ drinks >> it, emptying the cauldron. Lethargically, it << hoists itself ^ climbs back >> to its branch.",
"The imp produces a long metal straw and slurps up the brew from the cauldron.",
"The imp pushes down a lever, flushing the contents of cauldron.",
"The imp << stokes the ^ breathes >> fire, quickly boiling the brew until it evaporates. It licks clean the remaining sludge, emptying the cauldron."
])
if maker:
maker.announce_action(msg)
for item in self.contents:
item.delete()
def do_add_liquid(self, maker, obj):
"""
Like do_add, but doesn't delete the container.
Instead this creates a new "object" based on the contents of
the container. Makes it safe to delete.
"""
name, amount, desc = obj.do_empty()
if name:
for cup in range(int(amount / 4)):
liquid = spawn({
"typeclass": "typeclasses.objects.Object",
"key": f"cup of {name}"
})[0]
liquid.location = self
maker.announce_action(f"$You() $conj(pour) the {name} from $pron(your) {obj.name} into the {self.name}.")
else:
maker.msg(f"The {obj.name} is empty. You can |gfill|n it first.")
def do_add(self, maker, item):
"""
Moves an item to the cauldron.
If the item is a container, we call do_add_liquid to add its contents.
We limit this to Herbs and other consumables.
"""
if self.has_potion():
maker.msg("The cauldron already contains a potion. You need to |gempty|n it first.")
return
if item.has_method("do_empty"):
self.do_add_liquid(maker, item)
elif item.is_typeclass("typeclasses.consumables.Consumable") or \
item.is_typeclass("typeclasses.consumables.Herb"):
item.move_to(self, quiet=True)
maker.announce_action(f"$You() $conj(add) {item.name} to {self.name}.")
else:
maker.msg("Adding that to a cauldron for brewing potions doesn't make sense.")
def do_create(self, maker):
"""
Does this make a viable concoction?
"""
good = choice([
"The imp <<climbs ^ flies ^ jumps >> down from its perch, <<sniffs ^ tastes ^ samples >> the brew. Before returning to its roost, it << gives $you() a tiny thumbs up ^ nods ^ smiles ^ nods >>.",
])
bad = choice([
"The imp <<climbs ^ flies ^ jumps >> down from its perch, <<sniffs ^ tastes ^ samples >> the brew. Before returning to its roost, it << shakes its head ^ grimaces ^ retches a bit >>.",
"As soon as $you() $conj(grab) a wooden spoon to stir, the <<brew ^ concoction>> in the cauldron farts a black cloud. This must not be right."
])
if self.contents == []:
maker.msg(f"The {self.name} is empty. First, |gadd|n ingredients.")
return
seq, brew_func = self.can_create_laughter()
if seq:
maker.score(Scores.make_potion)
maker.announce_action(good)
for idx, msg in enumerate(seq):
delay(idx * 3 + 2, maker.announce_action, msg)
self.do_empty()
brew_func()
else:
maker.announce_action(bad)
def has_potion(self):
"""
Return reference to the potion in the cauldron.
None otherwise.
"""
if len(self.contents) == 1 and self.has(Potion):
return self.contents[0]
def do_bottle(self, maker):
"""
If the contents are viable, let's create a vial.
"""
potion = self.has_potion()
if not potion:
maker.msg("The cauldron doesn't have a potion to bottle. Perhaps, you need to |gcreate|n one first?")
return
vial = spawn({
"typeclass": "typeclasses.alchemy.Vial",
"key": potion.name,
"aliases": ["potion", "vial"],
"desc": potion.db.desc
})[0]
vial.db.amount = 1
vial.db.spell = potion.db.spell
vial.location = maker
maker.announce_action(f"$You() $conj(fill) a small vial with $pron(your) << stew ^ elixir ^ potion ^ concoction >>.")
# ----------------------------------------------------------------------
# Potions:
def can_create_laughter(self):
"""
Return true if the cauldron can make a laughter potion.
"""
mushrooms = self.search("gigglecap mushroom", location=self, quiet=True)
weeds = self.search("tickleweed", location=self, quiet=True)
water = self.search("fizzy water", location=self, quiet=True)
if len(mushrooms) > 0 and len(weeds) > 0 and len(water) > 1:
return ([
"$You() $conj(stir) the cauldron with a wooden spoon, watching the liquid change to a deep purple, releasing a fragrant aroma.",
"The imp climbs down from its perch and mutters, \"Risus ignis, laetitiae flamma, in corde nostro, gaudium humourous.\"",
"Sparks of vibrant octarine pop over the elixir, making it ready to |gbottle|n."
], self.create_laughter)
else:
return (None, None)
def create_laughter(self):
"""
Spawn a Potion with the spell function for the cauldron's contents.
"""
potion = spawn({
"typeclass": "typeclasses.alchemy.Potion",
"key": "potion",
"desc": "Small glass vial containing a purple liquid, labeled: |mElixir Risorium|n"
})[0]
potion.location = self
potion.db.spell = LaughterSpell
class Vial(Container):
"""
A vial with a single quaff, but cast a spell.
"""
fill_amount = 1
def at_object_creation(self):
"""
Set up the database.
"""
self.db.amount = 0
self.db.empty_name = "empty vial"
self.db.empty_desc = "Small crystal vial. May have contained a potion."
def do_drink(self, drinker):
"""
Called when owner calls the 'drink' command.
"""
if not self.do_empty():
drinker.msg("The vial is empty.")
else:
drinker.announce_action("$You() $conj(<< down ^ quaff ^ imbibe >>) a small vial.")
self.do_empty()
drinker.scripts.add(self.db.spell, autostart=True)
class Potion(Object):
"""
A marker-type object for a potion in the cauldron.
"""
pass
# ----------------------------------------------------------------------
# Scripts that emulate the effects of Potions
# ----------------------------------------------------------------------
class LaughterSpell(Script):
"""
This class defines the script itself
"""
def at_script_creation(self):
self.key = "laughter-spell"
self.desc = "Adds various timed events to a character."
self.interval = 20 # seconds
self.repeats = 15 # repeat only a certain number of times
self.start_delay = True # wait self.interval until first call
def at_repeat(self):
"""
This gets called every self.interval seconds. We make
a random check here so as to only return 33% of the time.
"""
if random() < 0.66:
# no message this time
return
self.send_random_message()
def send_random_message(self):
rand = randint(1, 10)
if rand == 1:
msg = "$You() $conj(erupt) into laughter, a deep, rolling sound that fills the room and makes everyone turn to see whats so funny."
elif rand == 2:
msg = "$You() $conj(let) out a series of high-pitched giggles, each one bubbling up like a fizzy drink, light and infectious."
elif rand == 3:
msg = "$You() $conj(burst) into cackling laughter, the sharp, gleeful sound that echo off the walls."
elif rand == 4:
msg = "$You() $conj(find) yourself wheezing with laughter, gasping for air as the hilarity overwhelms you, tears streaming down your cheeks."
elif rand == 5:
msg = "$You() $conj(break) into a fit of snorting giggles; you try to surpress them to no avail."
elif rand == 6:
msg = "$You() $conj(let) out a tinkling laugh, a melodic sound that dances through the air, bringing a sense of whimsy to the moment."
elif rand == 7:
msg = "$You() $conj(roar) with laughter, a booming sound that resonates with joy."
elif rand == 8:
msg = "$You() $conj(chuckle) softly, a warm and genuine sound that reflects your delight."
elif rand == 9:
msg = "$You() $conj(giggle) uncontrollably, making it hard for $pron(you) to catch $pron(your) breath."
else:
msg = "$You() $conj(let) a stifled chuckle, unsure what became so humorous."
self.obj.announce_action(msg)