Adding dark rooms

Decided that a Fireplace that is both a Pet and a Lightsource doesn't
get saved into the database. Might be a bug, but in the meantime,
Dabbler's house will softly be lit.
This commit is contained in:
Howard Abrams 2025-01-31 22:37:01 -08:00
parent af1655fadb
commit cec2de230d
5 changed files with 411 additions and 7 deletions

View file

@ -1,6 +1,5 @@
#!/usr/bin/env python
from commands.command import Command
from evennia import CmdSet

40
commands/lighting.py Executable file
View file

@ -0,0 +1,40 @@
#!/usr/bin/env python
from commands.command import Command
from evennia import CmdSet
class CmdLight(Command):
"""
Creates light where there was none. Something to burn.
"""
key = "light"
aliases = ["burn"]
# only allow this command if command.obj is carried by caller.
locks = "cmd:holds()"
def func(self):
"""
Implements the light command. Since this command is designed
to sit on a "lightable" object, we operate only on self.obj.
"""
if self.obj.light():
self.caller.msg("You light %s." % self.obj.key)
self.caller.location.msg_contents(
"%s lights %s!" % (self.caller, self.obj.key), exclude=[self.caller]
)
else:
self.caller.msg("%s is already burning." % self.obj.key)
class CmdSetLight(CmdSet):
"""CmdSet for the lightsource commands"""
key = "lightsource_cmdset"
# this is higher than the dark cmdset - important!
priority = 3
def at_cmdset_creation(self):
"""called at cmdset creation"""
self.add(CmdLight())

View file

@ -9,7 +9,9 @@ with a location in the game world (like Characters, Rooms, Exits).
"""
from evennia.objects.objects import DefaultObject
from evennia.utils import delay, search
from commands.lighting import CmdSetLight
class ObjectParent:
"""
@ -215,3 +217,97 @@ class Object(ObjectParent, DefaultObject):
"""
pass
# -------------------------------------------------------------
#
# LightSource
#
# This object emits light. Once it has been turned on it
# cannot be turned off. When it burns out it will delete
# itself.
#
# This could be implemented using a single-repeat Script or by
# registering with the TickerHandler. We do it simpler by
# using the delay() utility function. This is very simple
# to use but does not survive a server @reload. Because of
# where the light matters (in the Dark Room where you can
# find new light sources easily), this is okay here.
#
# -------------------------------------------------------------
class LightSource(Object):
"""
This implements a light source object.
When burned out, the object will be deleted.
"""
def at_init(self):
"""
If this is called with the Attribute is_giving_light already
set, we know that the timer got killed by a server
reload/reboot before it had time to finish. So we kill it here
instead. This is the price we pay for the simplicity of the
non-persistent delay() method.
"""
if self.db.is_giving_light:
self.delete()
def at_object_creation(self):
"""Called when object is first created."""
super().at_object_creation()
self.db.is_giving_light = False
self.db.burntime = 60 * 3 # 3 minutes
# this is the default desc, it can of course be customized
# when created.
self.db.desc = "A tapered candle."
# add the Light command
self.cmdset.add_default(CmdSetLight, persistent=True)
def _burnout(self):
"""
This is called when this light source burns out. We make no
use of the return value.
"""
# delete ourselves from the database
self.db.is_giving_light = False
try:
self.location.location.msg_contents(
"%s's %s flickers and dies." % (self.location, self.key), exclude=self.location
)
self.location.msg("Your %s flickers and dies." % self.key)
self.location.location.check_light_state()
except AttributeError:
try:
self.location.msg_contents("A %s on the floor flickers and dies." % self.key)
self.location.location.check_light_state()
except AttributeError:
# Mainly happens if we happen to be in a None location
pass
self.delete()
def light(self):
"""
Light this object - this is called by Light command.
"""
if self.db.is_giving_light:
return False
# burn for 3 minutes before calling _burnout
self.db.is_giving_light = True
# if we are in a dark room, trigger its light check
try:
self.location.location.check_light_state()
except AttributeError:
try:
# maybe we are directly in the room
self.location.check_light_state()
except AttributeError:
# we are in a None location
pass
finally:
# start the burn timer. When it runs out, self._burnout
# will be called. We store the deferred so it can be
# killed in unittesting.
self.deferred = delay(60 * 3, self._burnout)
return True

View file

@ -7,7 +7,7 @@ Each level of pet requires more aspects for interaction.
"""
from typeclasses.objects import Object
from typeclasses.objects import Object, LightSource
from commands.feedables import CmdFeed, CmdFeedSet
from utils.word_list import squish
@ -107,7 +107,7 @@ class Pet(Object):
# subscribe ourselves to a ticker to repeatedly call the hook
# "update_weather" on this object. The interval is randomized
# so as to not have all weather rooms update at the same time.
self.db.interval = random.randint(50, 70)
self.db.interval = random.randint(70, 90)
TICKER_HANDLER.add(interval=self.db.interval, callback=self.update_hunger)
def update_hunger(self, *args, **kwargs):
@ -120,11 +120,20 @@ class Pet(Object):
"""
curr = self.hunger()
amount = kwargs.get('amount', self.hungry_rate)
# self.location.msg_contents(f"Feeding a pet: {amount} {isinstance(amount, str)}-> {self.db.hunger_level} / {self.hungry_rate}")
self.db.hunger_level = self.db.hunger_level + amount
if self.db.hunger_level < 0:
self.db.hunger_level = 0
# if self.db.hunger_level < 5:
# self.db.is_giving_light = False
# else:
# self.db.is_giving_light = True
if self.hunger() != curr:
self.location.msg_contents("|w%s|n\n" % self.hunger_appearance())
self.location.check_light_state()
def return_appearance(self, looker, **kwargs):
"""
@ -138,6 +147,11 @@ class Pet(Object):
"""
return f"{self.db.desc} {self.hunger_appearance()}"
# ------------------------------------------------------------
# Fireplace, both a pet that is hungry and consumes food,
# but also emits light when it isn't starving.
# ------------------------------------------------------------
class Fire(Pet):
"""
Fire in this world, is a cute fireplace pet.
@ -164,8 +178,6 @@ class Fire(Pet):
}
def feed(self, feeder, args):
self.update_hunger(feeder=feeder, amount=300)
now = time()
last_fed = feeder.ndb.fire_last_fed # could be None
if last_fed and (now - last_fed < 30):
@ -181,6 +193,15 @@ class Fire(Pet):
get_up = "get up and"
gets_up = "gets up and"
feeder.msg(squish(f"You {get_up} put some {adj} wood on the fire in the fireplace."))
self.location.msg_contents(squish(f"{feeder.name} {gets_up} puts {adj} wood on the fire."),
if self.db.hunger_level < 5:
feeder.msg(squish(f"You {get_up} put some {adj} wood in the "
f"fireplace, and start a fire."))
self.location.msg_contents(squish(f"{feeder.name} {gets_up} starts a fire."),
exclude=feeder)
else:
feeder.msg(squish(f"You {get_up} put some {adj} wood on the "
f"fire in the fireplace."))
self.location.msg_contents(squish(f"{feeder.name} {gets_up} puts {adj} wood on the fire."),
exclude=feeder)
self.update_hunger(feeder=feeder, amount=300)

View file

@ -7,6 +7,10 @@ Rooms are simple containers that has no location of their own.
from evennia.objects.objects import DefaultRoom
from .objects import LightSource
import random
# the system error-handling module is defined in the settings. We load the
# given setting here using utils.object_from_module. This way we can use
# it regardless of if we change settings later.
@ -45,3 +49,247 @@ class Room(ObjectParent, DefaultRoom):
has_weather = False
pass
# -------------------------------------------------------------------------------
#
# Dark Room - a room with states
#
# This room limits the movemenets of its denizens unless they carry an active
# LightSource object (LightSource is defined in objects.LightSource)
#
# -------------------------------------------------------------------------------
DARK_MESSAGES = (
"It is pitch black. You are likely to be eaten by a grue.",
"It's pitch black. You fumble around but cannot find anything.",
"You don't see a thing. You feel around, managing to bump your fingers hard against something. Ouch!",
"You don't see a thing! Blindly grasping the air around you, you find nothing.",
"It's totally dark here. You almost stumble over something on the floor.",
"You are completely blind. For a moment you think you hear someone breathing nearby ... "
"\n ... surely you must be mistaken.",
)
ALREADY_LIGHTSOURCE = (
"You don't want to stumble around in blindness anymore. You already "
"found what you need. Let's get the fireplace lit already!"
)
FOUND_LIGHTSOURCE = (
"Your fingers bump against a candle on a candleabra."
"You pick it up, holding it firmly. Now you just need to"
" |wlight|n it using the flint and steel you carry with you."
)
class CmdLookDark(Command):
"""
Look around in darkness
Usage:
look
Look around in the darkness, trying
to find something.
"""
key = "look"
aliases = ["l", "feel", "search", "feel around", "fiddle"]
locks = "cmd:all()"
def func(self):
"""
Implement the command.
This works both as a look and a search command; there is a
random chance of eventually finding a light source.
"""
caller = self.caller
# count how many searches we've done
nr_searches = caller.ndb.dark_searches
if nr_searches is None:
nr_searches = 0
caller.ndb.dark_searches = nr_searches
if nr_searches < 4 and random.random() < 0.90:
# we don't find anything
caller.msg(random.choice(DARK_MESSAGES))
caller.ndb.dark_searches += 1
else:
# we could have found something!
if any(obj for obj in caller.contents if utils.inherits_from(obj, LightSource)):
# we already carry a LightSource object.
caller.msg(ALREADY_LIGHTSOURCE)
else:
# don't have a light source, create a new one.
create_object(LightSource, key="candle", location=caller)
caller.msg(FOUND_LIGHTSOURCE)
class CmdDarkHelp(Command):
"""
Help command for the dark state.
"""
key = "help"
locks = "cmd:all()"
def func(self):
"""
Replace the the help command with a not-so-useful help
"""
string = (
"Can't help you until you find some light! Try looking/feeling around for something to burn. "
"You shouldn't give up even if you don't find anything right away."
)
self.caller.msg(string)
class CmdDarkNoMatch(Command):
"""
This is a system command. Commands with special keys are used to
override special sitations in the game. The CMD_NOMATCH is used
when the given command is not found in the current command set (it
replaces Evennia's default behavior or offering command
suggestions)
"""
key = syscmdkeys.CMD_NOMATCH
locks = "cmd:all()"
def func(self):
"""Implements the command."""
self.caller.msg(
"Until you find some light, there's not much you can do. "
"Try feeling around, maybe you'll find something helpful!"
)
class DarkCmdSet(CmdSet):
"""
Groups the commands of the dark room together. We also import the
default say command here so that players can still talk in the
darkness.
We give the cmdset the mergetype "Replace" to make sure it
completely replaces whichever command set it is merged onto
(usually the default cmdset)
"""
key = "darkroom_cmdset"
mergetype = "Replace"
priority = 2
def at_cmdset_creation(self):
"""populate the cmdset."""
self.add(CmdLookDark())
self.add(CmdDarkHelp())
self.add(CmdDarkNoMatch())
self.add(default_cmds.CmdSay())
self.add(default_cmds.CmdQuit())
self.add(default_cmds.CmdHome())
class DarkRoom(Room):
"""A dark room. This tries to start the DarkState script on all
objects entering. The script is responsible for making sure it is
valid (that is, that there is no light source shining in the room).
The is_lit Attribute is used to define if the room is currently lit
or not, so as to properly echo state changes.
Since this room is meant as a sort of catch-all, we also make sure
to heal characters ending up here.
"""
def at_object_creation(self):
"""
Called when object is first created.
"""
super().at_object_creation()
# the room starts dark.
self.db.is_lit = False
self.cmdset.add(DarkCmdSet, persistent=True)
def at_init(self):
"""
Called when room is first recached (such as after a reload)
"""
self.check_light_state()
def _carries_light(self, obj):
"""
Checks if the given object carries anything that gives light.
Note that we do NOT look for a specific LightSource typeclass,
but for the Attribute is_giving_light - this makes it easy to
later add other types of light-giving items. We also accept
if there is a light-giving object in the room overall (like if
a candle was dropped in the room or the fireplace is lit).
"""
return (
obj.is_superuser
or obj.db.is_giving_light
or any(o for o in obj.contents if o.db.is_giving_light)
)
def _heal(self, character):
"""
Heal a character.
"""
health = character.db.health_max or 20
character.db.health = health
def check_light_state(self, exclude=None):
"""
This method checks if there are any light sources in the room.
If there isn't it makes sure to add the dark cmdset to all
characters in the room. It is called whenever characters enter
the room and also by the Light sources when they turn on.
Args:
exclude (Object): An object to not include in the light check.
"""
if any(self._carries_light(obj) for obj in self.contents if obj != exclude):
self.locks.add("view:all()")
self.cmdset.remove(DarkCmdSet)
self.db.is_lit = True
for char in (obj for obj in self.contents if obj.has_account):
# this won't do anything if it is already removed
char.msg("The room is lit up.")
else:
# noone is carrying light - darken the room
self.db.is_lit = False
self.locks.add("view:false()")
self.cmdset.add(DarkCmdSet, persistent=True)
for char in (obj for obj in self.contents if obj.has_account):
if char.is_superuser:
char.msg("You are Superuser, so you are not affected by the dark state.")
else:
# put players in darkness
char.msg("The room is completely dark.")
def at_object_receive(self, obj, source_location, move_type="move", **kwargs):
"""
Called when an object enters the room.
"""
if obj.has_account:
# a puppeted object, that is, a Character
self._heal(obj)
# in case the new guy carries light with them
self.check_light_state()
def at_object_leave(self, obj, target_location, move_type="move", **kwargs):
"""
In case people leave with the light, we make sure to clear the
DarkCmdSet if necessary. This also works if they are
teleported away.
"""
# since this hook is called while the object is still in the room,
# we exclude it from the light check, to ignore any light sources
# it may be carrying.
self.check_light_state(exclude=obj)
class DabblersRoom(Room):
pass