From 32fd9d2a4d4cc88a75989a121fcf9abed50df3da Mon Sep 17 00:00:00 2001 From: Griatch Date: Sun, 15 Feb 2015 21:30:52 +0100 Subject: [PATCH] Reworked tutorial world objects, starting with mob. --- evennia/contrib/tutorial_world/mob.py | 3 + evennia/contrib/tutorial_world/objects.py | 667 ++++++++++++---------- evennia/contrib/tutorial_world/rooms.py | 12 +- 3 files changed, 386 insertions(+), 296 deletions(-) diff --git a/evennia/contrib/tutorial_world/mob.py b/evennia/contrib/tutorial_world/mob.py index faffbfd5f0..2f1c3731af 100644 --- a/evennia/contrib/tutorial_world/mob.py +++ b/evennia/contrib/tutorial_world/mob.py @@ -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" diff --git a/evennia/contrib/tutorial_world/objects.py b/evennia/contrib/tutorial_world/objects.py index 6fa2ff5e75..0b6bbc0326 100644 --- a/evennia/contrib/tutorial_world/objects.py +++ b/evennia/contrib/tutorial_world/objects.py @@ -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 + + 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 + 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)) diff --git a/evennia/contrib/tutorial_world/rooms.py b/evennia/contrib/tutorial_world/rooms.py index fc8bce00c7..cce8c1222c 100644 --- a/evennia/contrib/tutorial_world/rooms.py +++ b/evennia/contrib/tutorial_world/rooms.py @@ -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): """