mirror of
https://github.com/evennia/evennia.git
synced 2026-03-16 21:06:30 +01:00
Reworked tutorial world objects, starting with mob.
This commit is contained in:
parent
0029232ab0
commit
32fd9d2a4d
3 changed files with 386 additions and 296 deletions
|
|
@ -41,6 +41,9 @@ class Mob(tut_objects.TutorialObject):
|
|||
self.db.last_location = None
|
||||
# only when True will the mob move.
|
||||
self.db.roam_mode = True
|
||||
#
|
||||
self.db.move_from
|
||||
self.location.msg_contents("With a cold breeze, %s drifts in the direction of %s." % (self.key, destination.key))
|
||||
|
||||
def announce_move_from(self, destination):
|
||||
"Called just before moving"
|
||||
|
|
|
|||
|
|
@ -24,6 +24,8 @@ import random
|
|||
|
||||
from evennia import create_object
|
||||
from evennia import DefaultObject, DefaultExit, Command, CmdSet, DefaultScript
|
||||
from evennia import utils
|
||||
from evennia.utils.spawner import spawn
|
||||
|
||||
#------------------------------------------------------------
|
||||
#
|
||||
|
|
@ -31,7 +33,7 @@ from evennia import DefaultObject, DefaultExit, Command, CmdSet, DefaultScript
|
|||
#
|
||||
# The TutorialObject is the base class for all items
|
||||
# in the tutorial. They have an attribute "tutorial_info"
|
||||
# on them that a global tutorial command can use to extract
|
||||
# on them that the global tutorial command can use to extract
|
||||
# interesting behind-the scenes information about the object.
|
||||
#
|
||||
# TutorialObjects may also be "reset". What the reset means
|
||||
|
|
@ -60,16 +62,20 @@ class TutorialObject(DefaultObject):
|
|||
|
||||
#------------------------------------------------------------
|
||||
#
|
||||
# Readable - an object one can "read".
|
||||
# Readable - an object that can be "read"
|
||||
#
|
||||
#------------------------------------------------------------
|
||||
|
||||
#
|
||||
# Read command
|
||||
#
|
||||
|
||||
class CmdRead(Command):
|
||||
"""
|
||||
Usage:
|
||||
read [obj]
|
||||
|
||||
Read some text.
|
||||
Read some text of a readable object.
|
||||
"""
|
||||
|
||||
key = "read"
|
||||
|
|
@ -77,7 +83,11 @@ class CmdRead(Command):
|
|||
help_category = "TutorialWorld"
|
||||
|
||||
def func(self):
|
||||
"Implement the read command."
|
||||
"""
|
||||
Implements the read command. This simply looks for an
|
||||
Attribute "readable_text" on the object and displays that.
|
||||
"""
|
||||
|
||||
if self.args:
|
||||
obj = self.caller.search(self.args.strip())
|
||||
else:
|
||||
|
|
@ -94,18 +104,25 @@ class CmdRead(Command):
|
|||
|
||||
|
||||
class CmdSetReadable(CmdSet):
|
||||
"CmdSet for readables"
|
||||
"""
|
||||
A CmdSet for readables.
|
||||
"""
|
||||
def at_cmdset_creation(self):
|
||||
"called when object is created."
|
||||
"""
|
||||
Called when the cmdset is created.
|
||||
"""
|
||||
self.add(CmdRead())
|
||||
|
||||
|
||||
class Readable(TutorialObject):
|
||||
"""
|
||||
This object defines some attributes and defines a read method on itself.
|
||||
This simple object defines some attributes and
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
"Called when object is created"
|
||||
"""
|
||||
Called when object is created. We make sure to set the needed
|
||||
Attribute and add the readable cmdset.
|
||||
"""
|
||||
super(Readable, self).at_object_creation()
|
||||
self.db.tutorial_info = "This is an object with a 'read' command defined in a command set on itself."
|
||||
self.db.readable_text = "There is no text written on %s." % self.key
|
||||
|
|
@ -119,14 +136,20 @@ class Readable(TutorialObject):
|
|||
#
|
||||
# The climbable object works so that once climbed, it sets
|
||||
# a flag on the climber to show that it was climbed. A simple
|
||||
# command 'climb' handles the actual climbing.
|
||||
# command 'climb' handles the actual climbing. The memory
|
||||
# of what was last climbed is used in a simple puzzle in the
|
||||
# tutorial.
|
||||
#
|
||||
#------------------------------------------------------------
|
||||
|
||||
class CmdClimb(Command):
|
||||
"""
|
||||
Climb an object
|
||||
|
||||
Usage:
|
||||
climb <object>
|
||||
|
||||
This allows you to climb.
|
||||
"""
|
||||
key = "climb"
|
||||
locks = "cmd:all()"
|
||||
|
|
@ -148,6 +171,7 @@ class CmdClimb(Command):
|
|||
if not ostring:
|
||||
ostring = "You climb %s. Having looked around, you climb down again." % self.obj.name
|
||||
self.caller.msg(ostring)
|
||||
# store this object to remember what we last climbed
|
||||
self.caller.db.last_climbed = self.obj
|
||||
|
||||
|
||||
|
|
@ -159,7 +183,10 @@ class CmdSetClimbable(CmdSet):
|
|||
|
||||
|
||||
class Climbable(TutorialObject):
|
||||
"A climbable object."
|
||||
"""
|
||||
A climbable object. All that is special about it is that it has
|
||||
the "climb" command available on it.
|
||||
"""
|
||||
|
||||
def at_object_creation(self):
|
||||
"Called at initial creation only"
|
||||
|
|
@ -171,21 +198,19 @@ class Climbable(TutorialObject):
|
|||
#
|
||||
# Obelisk - a unique item
|
||||
#
|
||||
# The Obelisk is an object with a modified return_appearance
|
||||
# method that causes it to look slightly different every
|
||||
# time one looks at it. Since what you actually see
|
||||
# is a part of a game puzzle, the act of looking also
|
||||
# stores a key attribute on the looking object for later
|
||||
# reference.
|
||||
# The Obelisk is an object with a modified return_appearance method
|
||||
# that causes it to look slightly different every time one looks at it.
|
||||
# Since what you actually see is a part of a game puzzle, the act of
|
||||
# looking also stores a key attribute on the looking object (different
|
||||
# depending on which text you saw) for later reference.
|
||||
#
|
||||
#------------------------------------------------------------
|
||||
|
||||
OBELISK_DESCS = ["You can briefly make out the image of {ba woman with a blue bird{n.",
|
||||
OBELISK_DESCS = ("You can briefly make out the image of {ba woman with a blue bird{n.",
|
||||
"You for a moment see the visage of {ba woman on a horse{n.",
|
||||
"For the briefest moment you make out an engraving of {ba regal woman wearing a crown{n.",
|
||||
"You think you can see the outline of {ba flaming shield{n in the stone.",
|
||||
"The surface for a moment seems to portray {ba woman fighting a beast{n."]
|
||||
|
||||
"The surface for a moment seems to portray {ba woman fighting a beast{n.")
|
||||
|
||||
class Obelisk(TutorialObject):
|
||||
"""
|
||||
|
|
@ -200,15 +225,22 @@ class Obelisk(TutorialObject):
|
|||
self.locks.add("get:false()")
|
||||
|
||||
def return_appearance(self, caller):
|
||||
"Overload the default version of this hook."
|
||||
"""
|
||||
This hook is called by the look command to get the description
|
||||
of the object. We overload it with our own version.
|
||||
"""
|
||||
# randomly get the index for one of the descriptions
|
||||
clueindex = random.randint(0, len(OBELISK_DESCS) - 1)
|
||||
# set this description
|
||||
string = "The surface of the obelisk seem to waver, shift and writhe under your gaze, with "
|
||||
string += "different scenes and structures appearing whenever you look at it. "
|
||||
# set this description, with the random extra
|
||||
string = "The surface of the obelisk seem to waver, shift and writhe under your gaze, with " \
|
||||
"different scenes and structures appearing whenever you look at it. "
|
||||
self.db.desc = string + OBELISK_DESCS[clueindex]
|
||||
# remember that this was the clue we got.
|
||||
# remember that this was the clue we got. The Puzzle room will
|
||||
# look for this later to determine if you should be teleported
|
||||
# or not.
|
||||
caller.db.puzzle_clue = clueindex
|
||||
# call the parent function as normal (this will use db.desc we just set)
|
||||
# call the parent function as normal (this will use
|
||||
# the new desc Attribute we just set)
|
||||
return super(Obelisk, self).return_appearance(caller)
|
||||
|
||||
|
||||
|
|
@ -216,164 +248,118 @@ class Obelisk(TutorialObject):
|
|||
#
|
||||
# LightSource
|
||||
#
|
||||
# This object that emits light and can be
|
||||
# turned on or off. It must be carried to use and has only
|
||||
# a limited burn-time.
|
||||
# When burned out, it will remove itself from the carrying
|
||||
# character's inventory.
|
||||
# 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 StateLightSourceOn(DefaultScript):
|
||||
class CmdLight(Command):
|
||||
"""
|
||||
This script controls how long the light source is burning. When
|
||||
it runs out of fuel, the lightsource goes out.
|
||||
"""
|
||||
def at_script_creation(self):
|
||||
"Called at creation of script."
|
||||
self.key = "lightsourceBurn"
|
||||
self.desc = "Keeps lightsources burning."
|
||||
self.start_delay = True # only fire after self.interval s.
|
||||
self.repeats = 1 # only run once.
|
||||
self.persistent = True # survive a server reboot.
|
||||
|
||||
def at_start(self):
|
||||
"Called at script start - this can also happen if server is restarted."
|
||||
self.interval = self.obj.db.burntime
|
||||
self.db.script_started = time.time()
|
||||
|
||||
def at_repeat(self):
|
||||
"Called at self.interval seconds"
|
||||
# this is only called when torch has burnt out
|
||||
self.obj.db.burntime = -1
|
||||
self.obj.reset()
|
||||
|
||||
def at_stop(self):
|
||||
"""
|
||||
Since the user may also turn off the light
|
||||
prematurely, this hook will store the current
|
||||
burntime.
|
||||
"""
|
||||
# calculate remaining burntime, if object is not
|
||||
# already deleted (because it burned out)
|
||||
if self.obj:
|
||||
try:
|
||||
time_burnt = time.time() - self.db.script_started
|
||||
except TypeError:
|
||||
# can happen if script_started is not defined
|
||||
time_burnt = self.interval
|
||||
burntime = self.interval - time_burnt
|
||||
self.obj.db.burntime = burntime
|
||||
|
||||
def is_valid(self):
|
||||
"This script is only valid as long as the lightsource burns."
|
||||
return self.obj.db.is_active
|
||||
|
||||
|
||||
class CmdLightSourceOn(Command):
|
||||
"""
|
||||
Switches on the lightsource.
|
||||
Creates light where there was none. Something to burn.
|
||||
"""
|
||||
key = "on"
|
||||
aliases = ["switch on", "turn on", "light"]
|
||||
locks = "cmd:holds()" # only allow if command.obj is carried by caller.
|
||||
aliases = ["light", "burn"]
|
||||
# only allow this command if command.obj is carried by caller.
|
||||
locks = "cmd:holds()"
|
||||
help_category = "TutorialWorld"
|
||||
|
||||
def func(self):
|
||||
"Implements the command"
|
||||
"""
|
||||
Implements the light command. Since this command is designed
|
||||
to sit on a "lightable" object, we operate only on self.obj.
|
||||
"""
|
||||
|
||||
if self.obj.db.is_active:
|
||||
self.caller.msg("%s is already burning." % self.obj.key)
|
||||
else:
|
||||
# set lightsource to active
|
||||
self.obj.db.is_active = True
|
||||
# activate the script to track burn-time.
|
||||
self.obj.scripts.add(StateLightSourceOn)
|
||||
self.caller.msg("{gYou light {C%s.{n" % self.obj.key)
|
||||
if self.obj.light():
|
||||
self.caller("You light %s." % self.obj.key)
|
||||
self.caller.location.msg_contents("%s lights %s!" % (self.caller, self.obj.key), exclude=[self.caller])
|
||||
# run script validation on the room to make light/dark states tick.
|
||||
self.caller.location.scripts.validate()
|
||||
# look around
|
||||
self.caller.execute_cmd("look")
|
||||
|
||||
|
||||
class CmdLightSourceOff(Command):
|
||||
"""
|
||||
Switch off the lightsource.
|
||||
"""
|
||||
key = "off"
|
||||
aliases = ["switch off", "turn off", "dowse"]
|
||||
locks = "cmd:holds()" # only allow if command.obj is carried by caller.
|
||||
help_category = "TutorialWorld"
|
||||
|
||||
def func(self):
|
||||
"Implements the command "
|
||||
|
||||
if not self.obj.db.is_active:
|
||||
self.caller.msg("%s is not burning." % self.obj.key)
|
||||
else:
|
||||
# set lightsource to inactive
|
||||
self.obj.db.is_active = False
|
||||
# validating the scripts will kill it now that is_active=False.
|
||||
self.obj.scripts.validate()
|
||||
self.caller.msg("{GYou dowse {C%s.{n" % self.obj.key)
|
||||
self.caller.location.msg_contents("%s dowses %s." % (self.caller, self.obj.key), exclude=[self.caller])
|
||||
self.caller.location.scripts.validate()
|
||||
self.caller.execute_cmd("look")
|
||||
self.caller.msg("%s is already burning." % self.obj.key)
|
||||
|
||||
|
||||
class CmdSetLightSource(CmdSet):
|
||||
class CmdSetLight(CmdSet):
|
||||
"CmdSet for the lightsource commands"
|
||||
key = "lightsource_cmdset"
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
"called at cmdset creation"
|
||||
self.add(CmdLightSourceOn())
|
||||
self.add(CmdLightSourceOff())
|
||||
self.add(CmdLight())
|
||||
|
||||
|
||||
class LightSource(TutorialObject):
|
||||
"""
|
||||
This implements a light source object.
|
||||
|
||||
When burned out, lightsource will be moved to its home - which by
|
||||
default is the location it was first created at.
|
||||
When burned out, the object will be deleted.
|
||||
"""
|
||||
def __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(LightSource, self).at_object_creation()
|
||||
self.db.tutorial_info = "This object can be turned on off and has a timed script controlling it."
|
||||
self.db.is_active = False
|
||||
self.db.tutorial_info = "This object can be lit to create light. It has a timeout for how long it burns."
|
||||
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 splinter of wood with remnants of resin on it, enough for burning."
|
||||
# add commands
|
||||
self.cmdset.add_default(CmdSetLightSource, permanent=True)
|
||||
# add the Light command
|
||||
self.cmdset.add_default(CmdSetLight, permanent=True)
|
||||
|
||||
def reset(self):
|
||||
def _burnout(self, ret):
|
||||
"""
|
||||
Can be called by tutorial world runner, or by the script when
|
||||
the lightsource has burned out.
|
||||
This is called when this light source burns out. We make no
|
||||
use of the return value.
|
||||
"""
|
||||
if self.db.burntime <= 0:
|
||||
# light burned out. Since the lightsources's "location" should be
|
||||
# a character, notify them this way.
|
||||
try:
|
||||
loc = self.location.location
|
||||
except AttributeError:
|
||||
loc = self.location
|
||||
loc.msg_contents("{c%s{n {Rburns out.{n" % self.key)
|
||||
self.db.is_active = False
|
||||
try:
|
||||
# validate in holders current room, if possible
|
||||
self.location.location.scripts.validate()
|
||||
# our location is usually a Character (their inventory), we try
|
||||
# to send to -their- location so everyone else also notices the
|
||||
# light goes out.
|
||||
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)
|
||||
except AttributeError:
|
||||
# maybe it was dropped, try validating at current location.
|
||||
try:
|
||||
self.location.scripts.validate()
|
||||
except AttributeError:
|
||||
pass
|
||||
# we are not in someone's inventory (maybe we were dropped.)
|
||||
self.location.msg_contents("A %s on the floor flickers and dies." % self.key)
|
||||
# delete ourselves.
|
||||
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.check_light_state()
|
||||
except Exception:
|
||||
# if we are not in a dark room, never mind
|
||||
pass
|
||||
finally:
|
||||
# start the burn timer. When it runs out, self._burnout
|
||||
# will be called.
|
||||
utils.delay(60 * 3, self._burnout)
|
||||
return True
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
#
|
||||
|
|
@ -382,7 +368,7 @@ class LightSource(TutorialObject):
|
|||
# This implements a simple puzzle exit that needs to be
|
||||
# accessed with commands before one can get to traverse it.
|
||||
#
|
||||
# The puzzle is currently simply to move roots (that have
|
||||
# The puzzle-part is simply to move roots (that have
|
||||
# presumably covered the wall) aside until a button for a
|
||||
# secret door is revealed. The original position of the
|
||||
# roots blocks the button, so they have to be moved to a certain
|
||||
|
|
@ -398,25 +384,34 @@ class LightSource(TutorialObject):
|
|||
# along the sides. The goal is to make the center position clear.
|
||||
# (yes, it's really as simple as it sounds, just move the roots
|
||||
# to each side to "win". This is just a tutorial, remember?)
|
||||
#
|
||||
# The ShiftRoot command depends on the root object having an
|
||||
# Attribute root_pos (a dictionary) to describe the current
|
||||
# position of the roots.
|
||||
|
||||
class CmdShiftRoot(Command):
|
||||
"""
|
||||
Shifts roots around.
|
||||
|
||||
shift blue root left/right
|
||||
shift red root left/right
|
||||
shift yellow root up/down
|
||||
shift green root up/down
|
||||
Usage:
|
||||
shift blue root left/right
|
||||
shift red root left/right
|
||||
shift yellow root up/down
|
||||
shift green root up/down
|
||||
|
||||
"""
|
||||
key = "shift"
|
||||
aliases = ["move"]
|
||||
# the locattr() lock looks for the attribute is_dark on the current room.
|
||||
locks = "cmd:not locattr(is_dark)"
|
||||
aliases = ["shiftroot", "push", "pull", "move"]
|
||||
# we only allow to use this command while the
|
||||
# room is properly lit, so we lock it to the
|
||||
# setting of Attribute "is_lit" on our location.
|
||||
locks = "cmd:locattr(is_lit)"
|
||||
help_category = "TutorialWorld"
|
||||
|
||||
def parse(self):
|
||||
"custom parser; split input by spaces"
|
||||
"""
|
||||
Custom parser; split input by spaces for simplicity.
|
||||
"""
|
||||
self.arglist = self.args.strip().split()
|
||||
|
||||
def func(self):
|
||||
|
|
@ -429,14 +424,20 @@ class CmdShiftRoot(Command):
|
|||
if not self.arglist:
|
||||
self.caller.msg("What do you want to move, and in what direction?")
|
||||
return
|
||||
|
||||
if "root" in self.arglist:
|
||||
# we clean out the use of the word "root"
|
||||
self.arglist.remove("root")
|
||||
|
||||
# we accept arguments on the form <color> <direction>
|
||||
|
||||
if not len(self.arglist) > 1:
|
||||
self.caller.msg("You must define which colour of root you want to move, and in which direction.")
|
||||
return
|
||||
|
||||
color = self.arglist[0].lower()
|
||||
direction = self.arglist[1].lower()
|
||||
|
||||
# get current root positions dict
|
||||
root_pos = self.obj.db.root_pos
|
||||
|
||||
|
|
@ -475,6 +476,7 @@ class CmdShiftRoot(Command):
|
|||
self.caller.msg("The thick reddish root gets in the way and is pushed back to the left.")
|
||||
else:
|
||||
self.caller.msg("You cannot move the root in that direction.")
|
||||
|
||||
# now the horizontal roots (yellow/green). They can be moved up/down
|
||||
elif color == "yellow":
|
||||
if direction == "up":
|
||||
|
|
@ -506,11 +508,14 @@ class CmdShiftRoot(Command):
|
|||
self.caller.msg("The root with yellow flowers gets in the way and is pushed upwards.")
|
||||
else:
|
||||
self.caller.msg("You cannot move the root in that direction.")
|
||||
# store new position
|
||||
|
||||
# we have moved the root. Store new position
|
||||
self.obj.db.root_pos = root_pos
|
||||
# check victory condition
|
||||
|
||||
# Check victory condition
|
||||
if root_pos.values().count(0) == 0: # no roots in middle position
|
||||
self.caller.db.crumbling_wall_found_button = True
|
||||
# This will affect the cmd: lock of CmdPressButton
|
||||
self.obj.db.button_exposed = True
|
||||
self.caller.msg("Holding aside the root you think you notice something behind it ...")
|
||||
|
||||
|
||||
|
|
@ -520,8 +525,11 @@ class CmdPressButton(Command):
|
|||
"""
|
||||
key = "press"
|
||||
aliases = ["press button", "button", "push", "push button"]
|
||||
# only accessible if the button was found and there is light.
|
||||
locks = "cmd:attr(crumbling_wall_found_button) and not locattr(is_dark)"
|
||||
# only accessible if the button was found and there is light. This checks
|
||||
# the Attribute button_exposed on the Wall object so that
|
||||
# you can only push the button when the puzzle is solved. It also
|
||||
# checks the is_lit Attribute on the location.
|
||||
locks = "cmd:objattr(button_exposed) and locattr(is_lit)"
|
||||
help_category = "TutorialWorld"
|
||||
|
||||
def func(self):
|
||||
|
|
@ -533,18 +541,16 @@ class CmdPressButton(Command):
|
|||
return
|
||||
|
||||
# pushing the button
|
||||
string = "You move your fingers over the suspicious depression, then gives it a "
|
||||
string += "decisive push. First nothing happens, then there is a rumble and a hidden "
|
||||
string += "{wpassage{n opens, dust and pebbles rumbling as part of the wall moves aside."
|
||||
string = "You move your fingers over the suspicious depression, then gives it a " \
|
||||
"decisive push. First nothing happens, then there is a rumble and a hidden " \
|
||||
"{wpassage{n opens, dust and pebbles rumbling as part of the wall moves aside."
|
||||
self.caller.msg(string)
|
||||
string = "%s moves their fingers over the suspicious depression, then gives it a " \
|
||||
"decisive push. First nothing happens, then there is a rumble and a hidden " \
|
||||
"{wpassage{n opens, dust and pebbles rumbling as part of the wall moves aside."
|
||||
self.caller.location.msg_contents(string % self.caller.key, exclude=self.caller)
|
||||
self.obj.open_wall()
|
||||
|
||||
# we are done - this will make the exit traversable!
|
||||
self.caller.db.crumbling_wall_found_exit = True
|
||||
# this will make it into a proper exit
|
||||
eloc = self.caller.search(self.obj.db.destination, global_search=True)
|
||||
if not eloc:
|
||||
self.caller.msg("The exit leads nowhere, there's just more stone behind it ...")
|
||||
return
|
||||
self.obj.destination = eloc
|
||||
self.caller.msg(string)
|
||||
|
||||
|
||||
|
|
@ -560,36 +566,30 @@ class CmdSetCrumblingWall(CmdSet):
|
|||
|
||||
class CrumblingWall(TutorialObject, DefaultExit):
|
||||
"""
|
||||
The CrumblingWall can be examined in various
|
||||
ways, but only if a lit light source is in the room. The traversal
|
||||
itself is blocked by a traverse: lock on the exit that only
|
||||
allows passage if a certain attribute is set on the trying
|
||||
player.
|
||||
This is a custom Exit.
|
||||
|
||||
The CrumblingWall can be examined in various ways, but only if a
|
||||
lit light source is in the room. The traversal itself is blocked
|
||||
by a traverse: lock on the exit that only allows passage if a
|
||||
certain attribute is set on the trying player.
|
||||
|
||||
Important attribute
|
||||
destination - this property must be set to make this a valid exit
|
||||
whenever the button is pushed (this hides it as an exit
|
||||
until it actually is)
|
||||
"""
|
||||
def __init__(self):
|
||||
"""
|
||||
We make sure to reset the puzzle after a server reload/reboot.
|
||||
"""
|
||||
self.reset()
|
||||
|
||||
def at_object_creation(self):
|
||||
"called when the object is first created."
|
||||
super(CrumblingWall, self).at_object_creation()
|
||||
|
||||
self.aliases.add(["secret passage", "passage",
|
||||
"crack", "opening", "secret door"])
|
||||
# this is assigned first when pushing button, so assign
|
||||
# this at creation time!
|
||||
|
||||
self.db.destination = 2
|
||||
# locks on the object directly transfer to the exit "command"
|
||||
self.locks.add("cmd:not locattr(is_dark)")
|
||||
|
||||
self.db.tutorial_info = "This is an Exit with a conditional traverse-lock. Try to shift the roots around."
|
||||
# the lock is important for this exit; we only allow passage
|
||||
# if we "found exit".
|
||||
self.locks.add("traverse:attr(crumbling_wall_found_exit)")
|
||||
# set cmdset
|
||||
self.cmdset.add(CmdSetCrumblingWall, permanent=True)
|
||||
|
||||
# starting root positions. H1/H2 are the horizontally hanging roots,
|
||||
# V1/V2 the vertically hanging ones. Each can have three positions:
|
||||
|
|
@ -598,6 +598,40 @@ class CrumblingWall(TutorialObject, DefaultExit):
|
|||
# ever any other identical value.
|
||||
self.db.root_pos = {"yellow": 0, "green": 0, "red": 0, "blue": 0}
|
||||
|
||||
# flags controlling the puzzle victory conditions
|
||||
self.db.button_exposed = False
|
||||
self.db.exit_open = False
|
||||
|
||||
# this is not even an Exit until it has a proper destination, and we won't assign
|
||||
# that until it is actually open. Until then we store the destination here. This
|
||||
# should be given a reasonable value at creation!
|
||||
self.db.destination = 2
|
||||
|
||||
# we lock this Exit so that one can only execute commands on it
|
||||
# if its location is lit and only traverse it once the Attribute
|
||||
# exit_open is set to True.
|
||||
self.locks.add("cmd:locattr(is_lit);traverse:objattr(exit_open)")
|
||||
|
||||
self.db.tutorial_info = "This is an Exit with a conditional traverse-lock. Try to shift the roots around."
|
||||
# set cmdset
|
||||
self.cmdset.add(CmdSetCrumblingWall, permanent=True)
|
||||
|
||||
def open(self):
|
||||
"""
|
||||
This method is called by the push button command once the puzzle
|
||||
is solved. It opens the wall and sets a timer for it to reset
|
||||
itself.
|
||||
"""
|
||||
# this will make it into a proper exit
|
||||
eloc = self.caller.search(self.obj.db.destination, global_search=True)
|
||||
if not eloc:
|
||||
self.caller.msg("The exit leads nowhere, there's just more stone behind it ...")
|
||||
else:
|
||||
self.obj.destination = eloc
|
||||
self.exit_open = True
|
||||
# start a 45 second timer before closing again
|
||||
utils.delay(45, self.reset)
|
||||
|
||||
def _translate_position(self, root, ipos):
|
||||
"Translates the position into words"
|
||||
rootnames = {"red": "The {rreddish{n vertical-hanging root ",
|
||||
|
|
@ -622,19 +656,28 @@ class CrumblingWall(TutorialObject, DefaultExit):
|
|||
This is called when someone looks at the wall. We need to echo the
|
||||
current root positions.
|
||||
"""
|
||||
if caller.db.crumbling_wall_found_button:
|
||||
string = "Having moved all the roots aside, you find that the center of the wall, "
|
||||
string += "previously hidden by the vegetation, hid a curious square depression. It was maybe once "
|
||||
string += "concealed and made to look a part of the wall, but with the crumbling of stone around it,"
|
||||
string += "it's now easily identifiable as some sort of button."
|
||||
if self.db.button_exposed:
|
||||
# we found the button by moving the roots
|
||||
string = "Having moved all the roots aside, you find that the center of the wall, " \
|
||||
"previously hidden by the vegetation, hid a curious square depression. It was maybe once " \
|
||||
"concealed and made to look a part of the wall, but with the crumbling of stone around it," \
|
||||
"it's now easily identifiable as some sort of button."
|
||||
elif self.db.exit_open:
|
||||
# we pressed the button; the exit is open
|
||||
string = "With the button pressed, a crack has opened in the root-covered wall, just wide enough " \
|
||||
"to squeeze through. A cold draft is coming from the hole and you get the feeling the " \
|
||||
"opening may close again soon."
|
||||
else:
|
||||
string = "The wall is old and covered with roots that here and there have permeated the stone. "
|
||||
string += "The roots (or whatever they are - some of them are covered in small non-descript flowers) "
|
||||
string += "crisscross the wall, making it hard to clearly see its stony surface.\n"
|
||||
# puzzle not solved yet.
|
||||
string = "The wall is old and covered with roots that here and there have permeated the stone. " \
|
||||
"The roots (or whatever they are - some of them are covered in small non-descript flowers) " \
|
||||
"crisscross the wall, making it hard to clearly see its stony surface.\n"
|
||||
# display the root positions to help with the puzzle
|
||||
for key, pos in self.db.root_pos.items():
|
||||
string += "\n" + self._translate_position(key, pos)
|
||||
self.db.desc = string
|
||||
# call the parent to continue execution (will use desc we just set)
|
||||
|
||||
# call the parent to continue execution (will use the desc we just set)
|
||||
return super(CrumblingWall, self).return_appearance(caller)
|
||||
|
||||
def at_after_traverse(self, traverser, source_location):
|
||||
|
|
@ -642,7 +685,7 @@ class CrumblingWall(TutorialObject, DefaultExit):
|
|||
This is called after we traversed this exit. Cleans up and resets
|
||||
the puzzle.
|
||||
"""
|
||||
del traverser.db.crumbling_wall_found_button
|
||||
del traverser.db.crumbling_wall_found_buttothe
|
||||
del traverser.db.crumbling_wall_found_exit
|
||||
self.reset()
|
||||
|
||||
|
|
@ -656,10 +699,11 @@ class CrumblingWall(TutorialObject, DefaultExit):
|
|||
traversed the Exit.
|
||||
"""
|
||||
self.location.msg_contents("The secret door closes abruptly, roots falling back into place.")
|
||||
for obj in self.location.contents:
|
||||
# clear eventual puzzle-solved attribues on everyone that didn't
|
||||
# get out in time. They have to try again.
|
||||
del obj.db.crumbling_wall_found_exit
|
||||
|
||||
# reset the flags and remove the exit destination
|
||||
self.db.button_exposed = False
|
||||
self.db.exit_open = False
|
||||
self.destination = None
|
||||
|
||||
# Reset the roots with some random starting positions for the roots:
|
||||
start_pos = [{"yellow":1, "green":0, "red":0, "blue":0},
|
||||
|
|
@ -667,8 +711,7 @@ class CrumblingWall(TutorialObject, DefaultExit):
|
|||
{"yellow":0, "green":1, "red":-1, "blue":0},
|
||||
{"yellow":1, "green":0, "red":0, "blue":0},
|
||||
{"yellow":0, "green":0, "red":0, "blue":1}]
|
||||
self.db.root_pos = start_pos[random.randint(0, 4)]
|
||||
self.destination = None
|
||||
self.db.root_pos = random.choice(start_pos)
|
||||
|
||||
|
||||
#------------------------------------------------------------
|
||||
|
|
@ -828,8 +871,104 @@ class Weapon(TutorialObject):
|
|||
#
|
||||
# Weapon rack - spawns weapons
|
||||
#
|
||||
# This is a spawner mechanism that creates custom weapons from a
|
||||
# spawner prototype dictionary. Note that we only create a single typeclass
|
||||
# (Weapon) yet customize all these different weapons using the spawner.
|
||||
# The spawner dictionaries could easily sit in separate modules and be
|
||||
# used to create unique and interesting variations of typeclassed
|
||||
# objects.
|
||||
#
|
||||
#------------------------------------------------------------
|
||||
|
||||
WEAPON_PROTOTYPES = {
|
||||
"weapon": {
|
||||
"typeclass": "tutorial_world.objects.Weapon",
|
||||
"key": "Weapon",
|
||||
"hit": 0.2,
|
||||
"parry": 0.2,
|
||||
"damage": 1.0,
|
||||
"magic": False,
|
||||
"desc": "A generic blade."},
|
||||
"knife": {
|
||||
"prototype": "weapon",
|
||||
"aliases": "sword",
|
||||
"key": "Kitchen knife",
|
||||
"desc":"A rusty kitchen knife. Better than nothing.",
|
||||
"damage": 3},
|
||||
"rusty dagger": {
|
||||
"prototype": "knife",
|
||||
"key": "Rusty dagger",
|
||||
"aliases": ["knife", "dagger"],
|
||||
"desc": "A double-edged dagger with a nicked edge and a wooden handle.",
|
||||
"hit": 0.25},
|
||||
"sword": {
|
||||
"prototype": "weapon",
|
||||
"key": "Rusty sword",
|
||||
"aliases": ["sword"],
|
||||
"desc": "A rusty shortsword. It has a leather-wrapped handle covered i food grease.",
|
||||
"hit": 0.3,
|
||||
"damage": 5,
|
||||
"parry": 0.5},
|
||||
"club": {
|
||||
"prototype": "weapon",
|
||||
"key":"Club",
|
||||
"desc": "A heavy wooden club, little more than a heavy branch.",
|
||||
"hit": 0.4,
|
||||
"damage": 6,
|
||||
"parry": 0.2},
|
||||
"ornate longsword": {
|
||||
"prototype":"sword",
|
||||
"key": "Ornate longsword",
|
||||
"desc": "A fine longsword with some swirling patterns on the handle.",
|
||||
"hit": 0.5,
|
||||
"damage": 5},
|
||||
"rune axe": {
|
||||
"prototype": "club",
|
||||
"key": "Runeaxe",
|
||||
"aliases": ["axe"],
|
||||
"hit": 0.4,
|
||||
"damage": 6},
|
||||
"thruning": {
|
||||
"prototype": "sword",
|
||||
"key": "Broadsword named Thruning",
|
||||
"desc": "This heavy bladed weapon is marked with the name 'Thruning'. It is very powerful in skilled hands.",
|
||||
"hit": 0.6,
|
||||
"parry": 0.6,
|
||||
"damage": 7},
|
||||
"warhammer": {
|
||||
"prototype": "club",
|
||||
"key": "Silver Warhammer",
|
||||
"aliases": ["hammer", "warhammer", "war"],
|
||||
"desc": "A heavy war hammer with silver ornaments. This huge weapon causes massive damage - if you can hit.",
|
||||
"hit": 0.4,
|
||||
"damage": 10},
|
||||
"slayer waraxe": {
|
||||
"prototype": "axe",
|
||||
"key": "Slayer waraxe",
|
||||
"aliases": ["waraxe", "war", "slayer"],
|
||||
"desc": "A huge double-bladed axe marked with the runes for 'Slayer'. It has more runic inscriptions on its head, which you cannot decipher.",
|
||||
"magic": True,
|
||||
"hit": 0.7,
|
||||
"damage": 8},
|
||||
"ghostblade": {
|
||||
"prototype": "slayer waraxe",
|
||||
"key": "The Ghostblade",
|
||||
"aliases": ["blade", "ghost"],
|
||||
"desc": "This massive sword is large as you are tall, yet seems to weigh almost nothing. It's almost like it's not really there.",
|
||||
"hit": 0.9,
|
||||
"parry": 0.8,
|
||||
"damage": 10},
|
||||
"hawkblade": {
|
||||
"prototype": "ghostblade",
|
||||
"key": "The Hawblade",
|
||||
"aliases": ["hawk", "blade"],
|
||||
"desc": "The weapon of a long-dead heroine and a more civilized age, the hawk-shaped hilt of this blade almost has a life of its own.",
|
||||
"hit": 0.95,
|
||||
"parry": 0.8,
|
||||
"damage": 12}
|
||||
}
|
||||
|
||||
|
||||
class CmdGetWeapon(Command):
|
||||
"""
|
||||
Usage:
|
||||
|
|
@ -837,40 +976,23 @@ class CmdGetWeapon(Command):
|
|||
|
||||
This will try to obtain a weapon from the container.
|
||||
"""
|
||||
key = "get"
|
||||
key = "get weapon"
|
||||
aliases = "get weapon"
|
||||
locks = "cmd:all()"
|
||||
help_cateogory = "TutorialWorld"
|
||||
|
||||
def func(self):
|
||||
"Implement the command"
|
||||
|
||||
rack_id = self.obj.db.rack_id
|
||||
if self.caller.attributes.get(rack_id):
|
||||
# we don't allow a player to take more than one weapon from rack.
|
||||
self.caller.msg("%s has no more to offer you." % self.obj.name)
|
||||
else:
|
||||
dmg, name, aliases, desc, magic = self.obj.randomize_type()
|
||||
new_weapon = create_object(Weapon, key=name, aliases=aliases,location=self.caller, home=self.caller)
|
||||
new_weapon.db.rack_id = rack_id
|
||||
new_weapon.db.damage = dmg
|
||||
new_weapon.db.desc = desc
|
||||
new_weapon.db.magic = magic
|
||||
ostring = self.obj.db.get_text
|
||||
if not ostring:
|
||||
ostring = "You pick up %s."
|
||||
if '%s' in ostring:
|
||||
self.caller.msg(ostring % name)
|
||||
else:
|
||||
self.caller.msg(ostring)
|
||||
# tag the caller so they cannot keep taking objects from the rack.
|
||||
self.caller.attributes.add(rack_id, True)
|
||||
|
||||
"""
|
||||
Get a weapon from the container. It will
|
||||
itself handle all messages.
|
||||
"""
|
||||
self.obj.produce_weapon(self.caller)
|
||||
|
||||
class CmdSetWeaponRack(CmdSet):
|
||||
"group the rack cmd"
|
||||
"""
|
||||
The cmdset for the rack.
|
||||
"""
|
||||
key = "weaponrack_cmdset"
|
||||
mergemode = "Replace"
|
||||
|
||||
def at_cmdset_creation(self):
|
||||
"Called at first creation of cmdset"
|
||||
|
|
@ -879,73 +1001,36 @@ class CmdSetWeaponRack(CmdSet):
|
|||
|
||||
class WeaponRack(TutorialObject):
|
||||
"""
|
||||
This will spawn a new weapon for the player unless the player already has
|
||||
one from this rack.
|
||||
This object represents a weapon store. When people use the
|
||||
"get weapon" command on this rack, it will produce one
|
||||
random weapon from among those registered to exist
|
||||
on it. This will also set a property on the character
|
||||
to make sure they can't get more than one at a time.
|
||||
|
||||
attribute to set at creation:
|
||||
min_dmg - the minimum damage of objects from this rack
|
||||
max_dmg - the maximum damage of objects from this rack
|
||||
magic - if weapons should be magical (have the magic flag set)
|
||||
get_text - the echo text to return when getting the weapon. Give '%s'
|
||||
to include the name of the weapon.
|
||||
"""
|
||||
def at_object_creation(self):
|
||||
"called at creation"
|
||||
"""
|
||||
called at creation
|
||||
"""
|
||||
self.cmdset.add_default(CmdSetWeaponRack, permanent=True)
|
||||
self.db.rack_id = "weaponrack_1"
|
||||
self.db.min_dmg = 1.0
|
||||
self.db.max_dmg = 4.0
|
||||
self.db.magic = False
|
||||
# these are prototype names from the prototype
|
||||
# dictionary above.
|
||||
self.db.available_weapons = ["knife", "rusty_dagger",
|
||||
"sword", "club"]
|
||||
|
||||
def randomize_type(self):
|
||||
def produce_weapon(self, caller):
|
||||
"""
|
||||
this returns a random weapon
|
||||
This will produce a new weapon from the rack,
|
||||
assuming the caller hasn't already gotten one.
|
||||
"""
|
||||
min_dmg = float(self.db.min_dmg)
|
||||
max_dmg = float(self.db.max_dmg)
|
||||
magic = bool(self.db.magic)
|
||||
dmg = min_dmg + random.random()*(max_dmg - min_dmg)
|
||||
aliases = [self.db.rack_id, "weapon"]
|
||||
if dmg < 1.5:
|
||||
name = "Knife"
|
||||
desc = "A rusty kitchen knife. Better than nothing."
|
||||
elif dmg < 2.0:
|
||||
name = "Rusty dagger"
|
||||
desc = "A double-edged dagger with nicked edge. It has a wooden handle."
|
||||
elif dmg < 3.0:
|
||||
name = "Sword"
|
||||
desc = "A rusty shortsword. It has leather wrapped around the handle."
|
||||
elif dmg < 4.0:
|
||||
name = "Club"
|
||||
desc = "A heavy wooden club with some rusty spikes in it."
|
||||
elif dmg < 5.0:
|
||||
name = "Ornate Longsword"
|
||||
aliases.extend(["longsword","ornate"])
|
||||
desc = "A fine longsword."
|
||||
elif dmg < 6.0:
|
||||
name = "Runeaxe"
|
||||
aliases.extend(["rune","axe"])
|
||||
desc = "A single-bladed axe, heavy but yet easy to use."
|
||||
elif dmg < 7.0:
|
||||
name = "Broadsword named Thruning"
|
||||
aliases.extend(["thruning","broadsword"])
|
||||
desc = "This heavy bladed weapon is marked with the name 'Thruning'. It is very powerful in skilled hands."
|
||||
elif dmg < 8.0:
|
||||
name = "Silver Warhammer"
|
||||
aliases.append("warhammer")
|
||||
desc = "A heavy war hammer with silver ornaments. This huge weapon causes massive damage."
|
||||
elif dmg < 9.0:
|
||||
name = "Slayer Waraxe"
|
||||
aliases.extend(["waraxe","slayer"])
|
||||
desc = "A huge double-bladed axe marked with the runes for 'Slayer'. It has more runic inscriptions on its head, which you cannot decipher."
|
||||
elif dmg < 10.0:
|
||||
name = "The Ghostblade"
|
||||
aliases.append("ghostblade")
|
||||
desc = "This massive sword is large as you are tall. Its metal shine with a bluish glow."
|
||||
if caller.attributes.get(self.db.rack_id):
|
||||
caller.msg("%s has no more to offer you." % self.key)
|
||||
else:
|
||||
name = "The Hawkblade"
|
||||
aliases.append("hawkblade")
|
||||
desc = "White surges of magical power runs up and down this runic blade. The hawks depicted on its hilt almost seems to have a life of their own."
|
||||
if dmg < 9 and magic:
|
||||
desc += "\nThe metal seems to glow faintly, as if imbued with more power than what is immediately apparent."
|
||||
return dmg, name, aliases, desc, magic
|
||||
prototype = random.choice(self.db.available_weapons)
|
||||
# use the spawner to create a new Weapon from the
|
||||
# spawner dictionary
|
||||
wpn = spawn(WEAPON_PROTOTYPES[prototype], prototype_parents=WEAPON_PROTOTYPES)
|
||||
caller.attributes.add(self.db.rack_id, True)
|
||||
wpn.location = caller
|
||||
caller.msg("You grab %s - %s." % (wpn.key, wpn.db.desc))
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@
|
|||
Room Typeclasses for the TutorialWorld.
|
||||
|
||||
This defines special types of Rooms available in the tutorial. To keep
|
||||
everything in one place we define them together with custom commands
|
||||
to control them, those commands could also have been in a separate
|
||||
module (e.g. if they could have been re-used elsewhere.)
|
||||
everything in one place we define them together with the custom
|
||||
commands needed to control them. Those commands could also have been
|
||||
in a separate module (e.g. if they could have been re-used elsewhere.)
|
||||
|
||||
"""
|
||||
|
||||
|
|
@ -301,9 +301,11 @@ class DarkRoom(TutorialRoom):
|
|||
|
||||
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.
|
||||
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 lantern was dropped in the room)
|
||||
"""
|
||||
return any(obj for obj in obj.contents if obj.db.is_giving_light)
|
||||
return obj.db.is_giving_light or any(obj for obj in obj.contents if obj.db.is_giving_light)
|
||||
|
||||
def _heal(self, character):
|
||||
"""
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue