"""
TutorialWorld - basic objects - Griatch 2011
This module holds all "dead" object definitions for
the tutorial world. Object-commands and -cmdsets
are also defined here, together with the object.
Objects:
TutorialObject
TutorialReadable
TutorialClimbable
Obelisk
LightSource
CrumblingWall
TutorialWeapon
TutorialWeaponRack
"""
import random
from evennia import CmdSet, Command, DefaultExit, DefaultObject
from evennia.prototypes.spawner import spawn
from evennia.utils import dedent, delay, search
# -------------------------------------------------------------
#
# TutorialObject
#
# The TutorialObject is the base class for all items
# in the tutorial. They have an attribute "tutorial_info"
# 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
# is up to the object. It can be the resetting of the world
# itself, or the removal of an inventory item from a
# character's inventory when leaving the tutorial, for example.
#
# -------------------------------------------------------------
[docs]class TutorialObject(DefaultObject):
"""
This is the baseclass for all objects in the tutorial.
"""
[docs] def at_object_creation(self):
"""Called when the object is first created."""
super().at_object_creation()
self.db.tutorial_info = "No tutorial info is available for this object."
[docs] def reset(self):
"""Resets the object, whatever that may mean."""
self.location = self.home
# -------------------------------------------------------------
#
# Readable - an object that can be "read"
#
# -------------------------------------------------------------
#
# Read command
#
[docs]class CmdRead(Command):
"""
Usage:
read [obj]
Read some text of a readable object.
"""
key = "read"
locks = "cmd:all()"
help_category = "TutorialWorld"
[docs] def func(self):
"""
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:
obj = self.obj
if not obj:
return
# we want an attribute read_text to be defined.
readtext = obj.db.readable_text
if readtext:
string = "You read |C%s|n:\n %s" % (obj.key, readtext)
else:
string = "There is nothing to read on %s." % obj.key
self.caller.msg(string)
[docs]class CmdSetReadable(CmdSet):
"""
A CmdSet for readables.
"""
[docs] def at_cmdset_creation(self):
"""
Called when the cmdset is created.
"""
self.add(CmdRead())
[docs]class TutorialReadable(TutorialObject):
"""
This simple object defines some attributes and
"""
[docs] def at_object_creation(self):
"""
Called when object is created. We make sure to set the needed
Attribute and add the readable cmdset.
"""
super().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
# define a command on the object.
self.cmdset.add_default(CmdSetReadable, persistent=True)
# -------------------------------------------------------------
#
# Climbable object
#
# 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. The memory
# of what was last climbed is used in a simple puzzle in the
# tutorial.
#
# -------------------------------------------------------------
[docs]class CmdClimb(Command):
"""
Climb an object
Usage:
climb <object>
This allows you to climb.
"""
key = "climb"
locks = "cmd:all()"
help_category = "TutorialWorld"
[docs] def func(self):
"""Implements function"""
if not self.args:
self.caller.msg("What do you want to climb?")
return
obj = self.caller.search(self.args.strip())
if not obj:
return
if obj != self.obj:
self.caller.msg("Try as you might, you cannot climb that.")
return
ostring = self.obj.db.climb_text
if not ostring:
ostring = "You climb %s. Having looked around, you climb down again." % self.obj.name
self.caller.msg(ostring)
# set a tag on the caller to remember that we climbed.
self.caller.tags.add("tutorial_climbed_tree", category="tutorial_world")
[docs]class CmdSetClimbable(CmdSet):
"""Climbing cmdset"""
[docs] def at_cmdset_creation(self):
"""populate set"""
self.add(CmdClimb())
[docs]class TutorialClimbable(TutorialObject):
"""
A climbable object. All that is special about it is that it has
the "climb" command available on it.
"""
[docs] def at_object_creation(self):
"""Called at initial creation only"""
self.cmdset.add_default(CmdSetClimbable, persistent=True)
# -------------------------------------------------------------
#
# 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 (different
# depending on which text you saw) for later reference.
#
# -------------------------------------------------------------
[docs]class Obelisk(TutorialObject):
"""
This object changes its description randomly, and which is shown
determines which order "clue id" is stored on the Character for
future puzzles.
Important Attribute:
puzzle_descs (list): list of descriptions. One of these is
picked randomly when this object is looked at and its index
in the list is used as a key for to solve the puzzle.
"""
[docs] def at_object_creation(self):
"""Called when object is created."""
super().at_object_creation()
self.db.tutorial_info = (
"This object changes its desc randomly, and makes sure to remember which one you saw."
)
self.db.puzzle_descs = ["You see a normal stone slab"]
# make sure this can never be picked up
self.locks.add("get:false()")
[docs] def return_appearance(self, caller):
"""
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
descs = self.db.puzzle_descs
clueindex = random.randint(0, len(descs) - 1)
# 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 + descs[clueindex]
# 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
# the new desc Attribute we just set)
return super().return_appearance(caller)
# -------------------------------------------------------------
#
# 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.
#
# -------------------------------------------------------------
[docs]class CmdLight(Command):
"""
Creates light where there was none. Something to burn.
"""
key = "on"
aliases = ["light", "burn"]
# only allow this command if command.obj is carried by caller.
locks = "cmd:holds()"
help_category = "TutorialWorld"
[docs] 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)
[docs]class CmdSetLight(CmdSet):
"""CmdSet for the lightsource commands"""
key = "lightsource_cmdset"
# this is higher than the dark cmdset - important!
priority = 3
[docs] def at_cmdset_creation(self):
"""called at cmdset creation"""
self.add(CmdLight())
[docs]class LightSource(TutorialObject):
"""
This implements a light source object.
When burned out, the object will be deleted.
"""
[docs] 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()
[docs] def at_object_creation(self):
"""Called when object is first created."""
super().at_object_creation()
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 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()
[docs] 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
# -------------------------------------------------------------
#
# Crumbling wall - unique exit
#
# This implements a simple puzzle exit that needs to be
# accessed with commands before one can get to traverse it.
#
# 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
# position - when they have, the "press button" command
# is made available and the Exit is made traversable.
#
# -------------------------------------------------------------
# There are four roots - two horizontal and two vertically
# running roots. Each can have three positions: top/middle/bottom
# and left/middle/right respectively. There can be any number of
# roots hanging through the middle position, but only one each
# 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.
[docs]class CmdShiftRoot(Command):
"""
Shifts roots around.
Usage:
shift blue root left/right
shift red root left/right
shift yellow root up/down
shift green root up/down
"""
key = "shift"
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"
[docs] def parse(self):
"""
Custom parser; split input by spaces for simplicity.
"""
self.arglist = self.args.strip().split()
[docs] def func(self):
"""
Implement the command.
blue/red - vertical roots
yellow/green - horizontal roots
"""
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
if color not in root_pos:
self.caller.msg("No such root to move.")
return
# first, vertical roots (red/blue) - can be moved left/right
if color == "red":
if direction == "left":
root_pos[color] = max(-1, root_pos[color] - 1)
self.caller.msg("You shift the reddish root to the left.")
if root_pos[color] != 0 and root_pos[color] == root_pos["blue"]:
root_pos["blue"] += 1
self.caller.msg(
"The root with blue flowers gets in the way and is pushed to the right."
)
elif direction == "right":
root_pos[color] = min(1, root_pos[color] + 1)
self.caller.msg("You shove the reddish root to the right.")
if root_pos[color] != 0 and root_pos[color] == root_pos["blue"]:
root_pos["blue"] -= 1
self.caller.msg(
"The root with blue flowers gets in the way and is pushed to the left."
)
else:
self.caller.msg(
"The root hangs straight down - you can only move it left or right."
)
elif color == "blue":
if direction == "left":
root_pos[color] = max(-1, root_pos[color] - 1)
self.caller.msg("You shift the root with small blue flowers to the left.")
if root_pos[color] != 0 and root_pos[color] == root_pos["red"]:
root_pos["red"] += 1
self.caller.msg(
"The reddish root is too big to fit as well, so that one falls away to the left."
)
elif direction == "right":
root_pos[color] = min(1, root_pos[color] + 1)
self.caller.msg("You shove the root adorned with small blue flowers to the right.")
if root_pos[color] != 0 and root_pos[color] == root_pos["red"]:
root_pos["red"] -= 1
self.caller.msg(
"The thick reddish root gets in the way and is pushed back to the left."
)
else:
self.caller.msg(
"The root hangs straight down - you can only move it left or right."
)
# now the horizontal roots (yellow/green). They can be moved up/down
elif color == "yellow":
if direction == "up":
root_pos[color] = max(-1, root_pos[color] - 1)
self.caller.msg("You shift the root with small yellow flowers upwards.")
if root_pos[color] != 0 and root_pos[color] == root_pos["green"]:
root_pos["green"] += 1
self.caller.msg("The green weedy root falls down.")
elif direction == "down":
root_pos[color] = min(1, root_pos[color] + 1)
self.caller.msg("You shove the root adorned with small yellow flowers downwards.")
if root_pos[color] != 0 and root_pos[color] == root_pos["green"]:
root_pos["green"] -= 1
self.caller.msg("The weedy green root is shifted upwards to make room.")
else:
self.caller.msg("The root hangs across the wall - you can only move it up or down.")
elif color == "green":
if direction == "up":
root_pos[color] = max(-1, root_pos[color] - 1)
self.caller.msg("You shift the weedy green root upwards.")
if root_pos[color] != 0 and root_pos[color] == root_pos["yellow"]:
root_pos["yellow"] += 1
self.caller.msg("The root with yellow flowers falls down.")
elif direction == "down":
root_pos[color] = min(1, root_pos[color] + 1)
self.caller.msg("You shove the weedy green root downwards.")
if root_pos[color] != 0 and root_pos[color] == root_pos["yellow"]:
root_pos["yellow"] -= 1
self.caller.msg(
"The root with yellow flowers gets in the way and is pushed upwards."
)
else:
self.caller.msg("The root hangs across the wall - you can only move it up or down.")
# we have moved the root. Store new position
self.obj.db.root_pos = root_pos
# Check victory condition
if list(root_pos.values()).count(0) == 0: # no roots in middle position
# 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 ...")
[docs]class CmdSetCrumblingWall(CmdSet):
"""Group the commands for crumblingWall"""
key = "crumblingwall_cmdset"
priority = 2
[docs] def at_cmdset_creation(self):
"""called when object is first created."""
self.add(CmdShiftRoot())
self.add(CmdPressButton())
[docs]class CrumblingWall(TutorialObject, DefaultExit):
"""
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 account.
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)
"""
[docs] def at_init(self):
"""
Called when object is recalled from cache.
"""
self.reset()
[docs] def at_object_creation(self):
"""called when the object is first created."""
super().at_object_creation()
self.aliases.add(["secret passage", "passage", "crack", "opening", "secret"])
# starting root positions. H1/H2 are the horizontally hanging roots,
# V1/V2 the vertically hanging ones. Each can have three positions:
# (-1, 0, 1) where 0 means the middle position. yellow/green are
# horizontal roots and red/blue vertical, all may have value 0, but n
# 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)")
# set cmdset
self.cmdset.add(CmdSetCrumblingWall, persistent=True)
[docs] def open_wall(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 (this returns a list)
eloc = search.search_object(self.db.destination)
if not eloc:
return False
else:
self.destination = eloc[0]
self.db.exit_open = True
# start a 45 second timer before closing again. We store the deferred so it can be
# killed in unittesting.
self.deferred = delay(45, self.reset)
return True
def _translate_position(self, root, ipos):
"""Translates the position into words"""
rootnames = {
"red": "The |rreddish|n vertical-hanging root ",
"blue": "The thick vertical root with |bblue|n flowers ",
"yellow": "The thin horizontal-hanging root with |yyellow|n flowers ",
"green": "The weedy |ggreen|n horizontal root ",
}
vpos = {
-1: "hangs far to the |wleft|n on the wall.",
0: "hangs straight down the |wmiddle|n of the wall.",
1: "hangs far to the |wright|n of the wall.",
}
hpos = {
-1: "covers the |wupper|n part of the wall.",
0: "passes right over the |wmiddle|n of the wall.",
1: "nearly touches the floor, near the |wbottom|n of the wall.",
}
if root in ("yellow", "green"):
string = rootnames[root] + hpos[ipos]
else:
string = rootnames[root] + vpos[ipos]
return string
[docs] def return_appearance(self, caller):
"""
This is called when someone looks at the wall. We need to echo the
current root positions.
"""
if self.db.button_exposed:
# we found the button by moving the roots
result = [
"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
result = [
"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:
# puzzle not solved yet.
result = [
"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 nondescript flowers) "
"crisscross the wall, making it hard to clearly see its stony surface. Maybe you could "
"try to |wshift|n or |wmove|n them (like '|wshift red up|n').\n"
]
# display the root positions to help with the puzzle
for key, pos in self.db.root_pos.items():
result.append("\n" + self._translate_position(key, pos))
self.db.desc = "".join(result)
# call the parent to continue execution (will use the desc we just set)
return super().return_appearance(caller)
[docs] def at_post_traverse(self, traverser, source_location):
"""
This is called after we traversed this exit. Cleans up and resets
the puzzle.
"""
del traverser.db.crumbling_wall_found_buttothe
del traverser.db.crumbling_wall_found_exit
self.reset()
[docs] def at_failed_traverse(self, traverser):
"""This is called if the account fails to pass the Exit."""
traverser.msg("No matter how you try, you cannot force yourself through %s." % self.key)
[docs] def reset(self):
"""
Called by tutorial world runner, or whenever someone successfully
traversed the Exit.
"""
self.location.msg_contents(
"The secret door closes abruptly, roots falling back into place."
)
# 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},
{"yellow": 0, "green": 0, "red": 0, "blue": 0},
{"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 = random.choice(start_pos)
# -------------------------------------------------------------
#
# TutorialWeapon - object type
#
# A weapon is necessary in order to fight in the tutorial
# world. A weapon (which here is assumed to be a bladed
# melee weapon for close combat) has three commands,
# stab, slash and defend. Weapons also have a property "magic"
# to determine if they are usable against certain enemies.
#
# Since Characters don't have special skills in the tutorial,
# we let the weapon itself determine how easy/hard it is
# to hit with it, and how much damage it can do.
#
# -------------------------------------------------------------
[docs]class CmdAttack(Command):
"""
Attack the enemy. Commands:
stab <enemy>
slash <enemy>
parry
stab - (thrust) makes a lot of damage but is harder to hit with.
slash - is easier to land, but does not make as much damage.
parry - forgoes your attack but will make you harder to hit on next
enemy attack.
"""
# this is an example of implementing many commands as a single
# command class, using the given command alias to separate between them.
key = "attack"
aliases = [
"hit",
"kill",
"fight",
"thrust",
"pierce",
"stab",
"slash",
"chop",
"bash",
"parry",
"defend",
]
locks = "cmd:all()"
help_category = "TutorialWorld"
[docs] def func(self):
"""Implements the stab"""
cmdstring = self.cmdstring
if cmdstring in ("attack", "fight"):
string = "How do you want to fight? Choose one of 'stab', 'slash' or 'defend'."
self.caller.msg(string)
return
# parry mode
if cmdstring in ("parry", "defend"):
string = (
"You raise your weapon in a defensive pose, ready to block the next enemy attack."
)
self.caller.msg(string)
self.caller.db.combat_parry_mode = True
self.caller.location.msg_contents(
"%s takes a defensive stance" % self.caller, exclude=[self.caller]
)
return
if not self.args:
self.caller.msg("Who do you attack?")
return
target = self.caller.search(self.args.strip())
if not target:
return
if cmdstring in ("thrust", "pierce", "stab"):
hit = float(self.obj.db.hit) * 0.7 # modified due to stab
damage = self.obj.db.damage * 2 # modified due to stab
string = "You stab with %s. " % self.obj.key
tstring = "%s stabs at you with %s. " % (self.caller.key, self.obj.key)
ostring = "%s stabs at %s with %s. " % (self.caller.key, target.key, self.obj.key)
self.caller.db.combat_parry_mode = False
elif cmdstring in ("slash", "chop", "bash"):
hit = float(self.obj.db.hit) # un modified due to slash
damage = self.obj.db.damage # un modified due to slash
string = "You slash with %s. " % self.obj.key
tstring = "%s slash at you with %s. " % (self.caller.key, self.obj.key)
ostring = "%s slash at %s with %s. " % (self.caller.key, target.key, self.obj.key)
self.caller.db.combat_parry_mode = False
else:
self.caller.msg(
"You fumble with your weapon, unsure of whether to stab, slash or parry ..."
)
self.caller.location.msg_contents(
"%s fumbles with their weapon." % self.caller, exclude=self.caller
)
self.caller.db.combat_parry_mode = False
return
if target.db.combat_parry_mode:
# target is defensive; even harder to hit!
target.msg("|GYou defend, trying to avoid the attack.|n")
hit *= 0.5
if random.random() <= hit:
self.caller.msg(string + "|gIt's a hit!|n")
target.msg(tstring + "|rIt's a hit!|n")
self.caller.location.msg_contents(
ostring + "It's a hit!", exclude=[target, self.caller]
)
# call enemy hook
if hasattr(target, "at_hit"):
# should return True if target is defeated, False otherwise.
target.at_hit(self.obj, self.caller, damage)
return
elif target.db.health:
target.db.health -= damage
else:
# sorry, impossible to fight this enemy ...
self.caller.msg("The enemy seems unaffected.")
return
else:
self.caller.msg(string + "|rYou miss.|n")
target.msg(tstring + "|gThey miss you.|n")
self.caller.location.msg_contents(ostring + "They miss.", exclude=[target, self.caller])
[docs]class CmdSetWeapon(CmdSet):
"""Holds the attack command."""
[docs] def at_cmdset_creation(self):
"""called at first object creation."""
self.add(CmdAttack())
[docs]class TutorialWeapon(TutorialObject):
"""
This defines a bladed weapon.
Important attributes (set at creation):
hit - chance to hit (0-1)
parry - chance to parry (0-1)
damage - base damage given (modified by hit success and
type of attack) (0-10)
"""
[docs] def at_object_creation(self):
"""Called at first creation of the object"""
super().at_object_creation()
self.db.hit = 0.4 # hit chance
self.db.parry = 0.8 # parry chance
self.db.damage = 1.0
self.db.magic = False
self.cmdset.add_default(CmdSetWeapon, persistent=True)
[docs] def reset(self):
"""
When reset, the weapon is simply deleted, unless it has a place
to return to.
"""
if self.location.has_account and self.home == self.location:
self.location.msg_contents(
"%s suddenly and magically fades into nothingness, as if it was never there ..."
% self.key
)
self.delete()
else:
self.location = self.home
# -------------------------------------------------------------
#
# 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": "evennia.contrib.tutorials.tutorial_world.objects.TutorialWeapon",
"key": "Weapon",
"hit": 0.2,
"parry": 0.2,
"damage": 1.0,
"magic": False,
"desc": "A generic blade.",
},
"knife": {
"prototype_parent": "weapon",
"aliases": "sword",
"key": "Kitchen knife",
"desc": "A rusty kitchen knife. Better than nothing.",
"damage": 3,
},
"dagger": {
"prototype_parent": "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_parent": "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_parent": "weapon",
"key": "Club",
"desc": "A heavy wooden club, little more than a heavy branch.",
"hit": 0.4,
"damage": 6,
"parry": 0.2,
},
"axe": {
"prototype_parent": "weapon",
"key": "Axe",
"desc": "A woodcutter's axe with a keen edge.",
"hit": 0.4,
"damage": 6,
"parry": 0.2,
},
"ornate longsword": {
"prototype_parent": "sword",
"key": "Ornate longsword",
"desc": "A fine longsword with some swirling patterns on the handle.",
"hit": 0.5,
"magic": True,
"damage": 5,
},
"warhammer": {
"prototype_parent": "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,
"magic": True,
"damage": 8,
},
"rune axe": {
"prototype_parent": "axe",
"key": "Runeaxe",
"aliases": ["axe"],
"hit": 0.4,
"magic": True,
"damage": 6,
},
"thruning": {
"prototype_parent": "ornate longsword",
"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,
},
"slayer waraxe": {
"prototype_parent": "rune 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.",
"hit": 0.7,
"damage": 8,
},
"ghostblade": {
"prototype_parent": "ornate longsword",
"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_parent": "ghostblade",
"key": "The Hawkblade",
"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.85,
"parry": 0.7,
"damage": 11,
},
}
[docs]class CmdGetWeapon(Command):
"""
Usage:
get weapon
This will try to obtain a weapon from the container.
"""
key = "get weapon"
aliases = "get weapon"
locks = "cmd:all()"
help_category = "TutorialWorld"
[docs] def func(self):
"""
Get a weapon from the container. It will
itself handle all messages.
"""
self.obj.produce_weapon(self.caller)
[docs]class CmdSetWeaponRack(CmdSet):
"""
The cmdset for the rack.
"""
key = "weaponrack_cmdset"
[docs] def at_cmdset_creation(self):
"""Called at first creation of cmdset"""
self.add(CmdGetWeapon())
[docs]class TutorialWeaponRack(TutorialObject):
"""
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.
Attributes to set on this object:
available_weapons: list of prototype-keys from
WEAPON_PROTOTYPES, the weapons available in this rack.
no_more_weapons_msg - error message to return to accounts
who already got one weapon from the rack and tries to
grab another one.
"""
[docs] def at_object_creation(self):
"""
called at creation
"""
self.cmdset.add_default(CmdSetWeaponRack, persistent=True)
self.db.rack_id = "weaponrack_1"
# these are prototype names from the prototype
# dictionary above.
self.db.get_weapon_msg = dedent(
"""
You find |c%s|n. While carrying this weapon, these actions are available:
|wstab/thrust/pierce <target>|n - poke at the enemy. More damage but harder to hit.
|wslash/chop/bash <target>|n - swipe at the enemy. Less damage but easier to hit.
|wdefend/parry|n - protect yourself and make yourself harder to hit.)
"""
).strip()
self.db.no_more_weapons_msg = "you find nothing else of use."
self.db.available_weapons = ["knife", "dagger", "sword", "club"]
[docs] def produce_weapon(self, caller):
"""
This will produce a new weapon from the rack,
assuming the caller hasn't already gotten one. When
doing so, the caller will get Tagged with the id
of this rack, to make sure they cannot keep
pulling weapons from it indefinitely.
"""
rack_id = self.db.rack_id
if caller.tags.get(rack_id, category="tutorial_world"):
caller.msg(self.db.no_more_weapons_msg)
else:
prototype = random.choice(self.db.available_weapons)
# use the spawner to create a new Weapon from the
# spawner dictionary, tag the caller
wpn = spawn(WEAPON_PROTOTYPES[prototype], prototype_parents=WEAPON_PROTOTYPES)[0]
caller.tags.add(rack_id, category="tutorial_world")
wpn.location = caller
caller.msg(self.db.get_weapon_msg % wpn.key)