diff --git a/evennia/__init__.py b/evennia/__init__.py index 03aaf9e4ec..51b4ef5245 100644 --- a/evennia/__init__.py +++ b/evennia/__init__.py @@ -88,7 +88,9 @@ except (IOError, CalledProcessError): def init(): """ - This is called only after Evennia has fully initialized all its models. + This is called by the launcher only after Evennia has fully + initialized all its models. It sets up the API in a safe + environment where all models are available already. """ def imp(path, variable=True): "Helper function" @@ -97,14 +99,14 @@ def init(): mod, fromlist = path.rsplit('.', 1) return __import__(mod, fromlist=[fromlist]) - global DefaultPlayer, DefaultObject, DefaultGuest, DefaultCharacter, \ - DefaultRoom, DefaultExit, DefaultChannel, DefaultScript + global DefaultPlayer, DefaultObject, DefaultGuest, DefaultCharacter + global DefaultRoom, DefaultExit, DefaultChannel, DefaultScript global ObjectDB, PlayerDB, ScriptDB, ChannelDB, Msg global Command, CmdSet, default_cmds, syscmdkeys global search_object, search_script, search_player, search_channel, search_help global create_object, create_script, create_player, create_channel, create_message - global lockfuncs, tickerhandler, logger, utils, gametime, ansi, spawn, managers - global contrib + global lockfuncs, logger, utils, gametime, ansi, spawn, managers + global contrib, TICKER_HANDLER, OOB_HANDLER, SESSION_HANDLER, CHANNEL_HANDLER from players.players import DefaultPlayer from players.players import DefaultGuest diff --git a/evennia/commands/cmdhandler.py b/evennia/commands/cmdhandler.py index 5636b3f6b3..57e193400c 100644 --- a/evennia/commands/cmdhandler.py +++ b/evennia/commands/cmdhandler.py @@ -146,6 +146,7 @@ def get_and_merge_cmdsets(caller, session, player, obj, yield [lobj.cmdset.current for lobj in local_objlist if (lobj.cmdset.current and lobj.locks.check(caller, 'call', no_superuser_bypass=True))] + print "local_obj_cmdsets:", [c.key for c in local_obj_cmdsets] for cset in local_obj_cmdsets: #This is necessary for object sets, or we won't be able to # separate the command sets from each other in a busy room. @@ -209,6 +210,7 @@ def get_and_merge_cmdsets(caller, session, player, obj, if cmdsets: # faster to do tuple on list than to build tuple directly mergehash = tuple([id(cmdset) for cmdset in cmdsets]) + print "mergehash:", mergehash, mergehash in _CMDSET_MERGE_CACHE, [(c.key, c.priority) for c in cmdsets] if mergehash in _CMDSET_MERGE_CACHE: # cached merge exist; use that cmdset = _CMDSET_MERGE_CACHE[mergehash] diff --git a/evennia/commands/default/batchprocess.py b/evennia/commands/default/batchprocess.py index f16a3ced7e..1e2a452388 100644 --- a/evennia/commands/default/batchprocess.py +++ b/evennia/commands/default/batchprocess.py @@ -694,11 +694,11 @@ class CmdStateCC(MuxCommand): class CmdStateJJ(MuxCommand): """ - j + jj Jump to specific command number """ - key = "j" + key = "jj" help_category = "BatchProcess" locks = "cmd:perm(batchcommands)" diff --git a/evennia/commands/default/general.py b/evennia/commands/default/general.py index 9b8f0a9fa2..7fbfc9de80 100644 --- a/evennia/commands/default/general.py +++ b/evennia/commands/default/general.py @@ -54,65 +54,33 @@ class CmdLook(MuxCommand): locks = "cmd:all()" arg_regex = r"\s|$" - # we split up the functionality of Look a little - # since this is a command which is very common to - # overload; this makes it easy to overload different - # sections of it without overloading all. - def at_found_target(self, target): - """ - Called when a target object has been found to look at. - - Args: - target (Object): object found to look at (args have - been parsed and searched for already at this point) - - The default implementation calls the return_appearance hook on - the observed target object to have it describe itself (and - possibly its contents) as well as format the description into - a suitable form. - """ - caller = self.caller - if not hasattr(target, 'return_appearance'): - # this is likely due to us having a player instead - target = target.character - if not target.access(caller, "view"): - # no permission to view this object - act as if - # it was not found at all. - caller.msg("Could not find '%s'." % self.args) - return - # get object's appearance - self.caller.msg(target.return_appearance(caller)) - # the object's at_desc() method. - target.at_desc(looker=caller) - - def at_not_found_target(self): - """ - Called when no target object was found to look at. - """ - if not self.args: - # this means we tried to look at location but failed. It - # usually means we are OOC. - return self.caller.msg("You have no location to look at!") - # otherwise we just return quietly. - return - def func(self): """ Handle the looking. """ caller = self.caller - if self.args: - target = caller.search(self.args, use_nicks=True) - if target: - return self.at_found_target(target) - else: - return self.at_not_found_target() + args = self.args + if args: + # Use search to handle duplicate/nonexistant results. + looking_at_obj = caller.search(args, use_nicks=True) + if not looking_at_obj: + return else: - target = caller.location - if target: - return self.at_found_target(target) - else: - return self.at_not_found_target() + looking_at_obj = caller.location + if not looking_at_obj: + caller.msg("You have no location to look at!") + return + + if not hasattr(looking_at_obj, 'return_appearance'): + # this is likely due to us having a player instead + looking_at_obj = looking_at_obj.character + if not looking_at_obj.access(caller, "view"): + caller.msg("Could not find '%s'." % args) + return + # get object's appearance + caller.msg(looking_at_obj.return_appearance(caller)) + # the object's at_desc() method. + looking_at_obj.at_desc(looker=caller) class CmdNick(MuxCommand): diff --git a/evennia/contrib/tutorial_world/build.ev b/evennia/contrib/tutorial_world/build.ev index 1b36b33860..b809e3693c 100644 --- a/evennia/contrib/tutorial_world/build.ev +++ b/evennia/contrib/tutorial_world/build.ev @@ -75,10 +75,12 @@ # # Build the intro room (don't forget to also connect the outro room to this later) # -# Note the unique id tut#XX we give each room. One empty line results in a -# line-break in the game, whereas two lines create a new -# paragraph. The length of the lines in the batchfile does not matter, -# in-game they will fill the lines to the width as defined by the +# Note the unique alias tut#XX we give each room. This is used to +# easily reference this object from other objects in the build script +# without knowing the dbref. One empty line results in a line-break in +# the game, whereas two lines create a new paragraph. The length of the +# lines in the batchfile does not matter, in-game they will fill the +# lines to the width as defined by the # player's client. # @dig Intro;tut#01 @@ -125,20 +127,21 @@ tutorial along a high, rocky coast ... - {g(write 'start' or 'begin' to start the tutorial){n + {g(write 'start' or 'begin' to start the tutorial. Try 'tutorial' + to get behind-the-scenes help anywhere.){n # # Show that the tutorial command works ... # @set here/tutorial_info = - This is the tutorial command. Use it in various rooms to see what's - technically going on and what you could try in each room. The intro - room assigns some properties to your character, like a simple - "health" property used when fighting. Other rooms and puzzles might - do the same. Leaving the tutorial world through any of the normal - exit rooms will clean away all such temporary properties. + You just tried the tutorial command. Use it in various rooms to see +what's technically going on and what you could try in each room. The +intro room assigns some properties to your character, like a simple +"health" property used when fighting. Other rooms and puzzles might do +the same. Leaving the tutorial world through any of the normal exit +rooms will clean away all such temporary properties. - If you play this scenario as superuser, you will see a big red - warning. This warning is generated in the intro-rooms Typeclass. +If you play this scenario as superuser, you will see a big red +warning. This warning is generated in the intro-rooms Typeclass. #------------------------------------------------------------ # @@ -179,34 +182,60 @@ start # exits. Note the alias tut#02: this unique identifier can be used # later in the script to always find the way back to this room (for # example by teleporting and similar). This is necessary since there -# is no way of knowing what dbref a given room will get in the +# is no way of knowing beforehand what dbref a given room will get in the # database. # -@dig/teleport Cliff by the sea;cliff;tut#02 +@dig/teleport Cliff by the coast;cliff;tut#02 : tutorial_world.rooms.WeatherRoom = begin adventure;begin;start # -# We define the tutorial message seen when the using the tutorial command +# We define the tutorial message seen when using the tutorial command # @set here/tutorial_info = Weather room - This room inherits from a parent called WeatherRoom. It runs on a - timer-Script that allows various weather-related messages to appear - at irregular intervals. + This room inherits from a parent called WeatherRoom. It uses the + tickerhandler to regularly 'tick and randomly display various + weather-related messages. + + The room also has 'details' set on it (such as the old well), those + are snippets of text stored on the room that the custom look command + used for all tutorial rooms can display. # @desc - You stand on the high coast line overlooking a stormy sea far + You stand on the high coast line overlooking a stormy {wsea{n far below. Around you the ground is covered in low gray-green grass, pushed flat by wind and rain. Inland, the vast dark moors begin, only here and there covered in patches of low trees and brushes. - - To the east, you glimpse the ragged outline of a castle ruin. It sits + To the east, you glimpse the ragged outline of a castle {wruin{n. It sits perched on a sheer cliff out into the water, isolated from the shore. The only way to reach it seems by way of an old hanging bridge, anchored not far east from here. # +# Mood-setting details to look at. This makes use of the custom look +# command in use on tutorial rooms to display extra text strings. It +# adds the detail as a dictionary Attribute on the room. +# +@detail ruin;ruins;castle = + A fair bit out from the rocky shores you can make out the foggy + outlines of a ruined castle. The once mighty towers have crumbled and + it presents a jagged shape against the rainy sky. The ruin is perched + on its own cliff, only connected to the mainland by means of an old + hanging bridge starting not far east from you. +# +@detail sea;ocean;waves = + The gray sea stretches as far as the eye can see to the east. Far + below you its waves crash against the foot of the cliff. The vast + inland moor meets the ocean along a high and uninviting coastline of + ragged vertical stone. + + Once this part of the world might have been beautiful, but now the + eternal winds and storms have washed it all down into a gray and + barren wasteland. +# +@detail +# # This is the well you will come back up from if you end up in the underground. # @create/drop Old well;well @@ -231,9 +260,10 @@ start all those stones? Start your own quarry?). # @set well/tutorial_info = - This is a normal object, locked with get:false() so that Players - cannot pick it up. Since the get_err property is set, you get a - customized error message when trying to pick it up. + This is a normal object, locked with the lock get:false() so that + Characters can't pick it up. Since the get_err Attribute is also set, + you get a customized error message when trying to pick it up (that + is checked and echoed by the 'get' command). # @create/drop Wooden sign;sign : tutorial_world.objects.Readable # @@ -248,62 +278,22 @@ start @set sign/get_err_msg = The sign is securely anchored to the ground. # @set sign/readable_text = - WARNING - Bridge is not safe! + + + WARNING - The bridge is not safe! + + + Below this official warning, someone has carved some sprawling + letters into the wood. It reads: "The guardian will not bleed to + mortal blade." # @set sign/tutorial_info = This is a readable object, of the Typeclass - contrib.tutorial_world.objects.Readable. The sign has a cmdset + evennia.contrib.tutorial_world.objects.Readable. The sign has a cmdset defined on itself, containing only one command, namely 'read'. This command is what allows you to 'read sign'. Doing so returns the - contents of an attribute containing the information on the sign. -# -# Mood-setting objects to look at -# -@create/drop ruin (in the distance);castle;ruin -# -@desc ruin = - A fair bit out from the rocky shores you can make out the foggy - outlines of a ruined castle. The once mighty towers have crumbled and - it presents a jagged shape against the rainy sky. The ruin is perched - on its own cliff, only connected to the mainland by means of an old - hanging bridge starting not far east from you. -# -@lock ruin = get:false() -# -@set ruin/get_err_msg = - Small as it may appear from a distance, you still cannot reach over and - pick up the castle to put in your pocket. -# -@set ruin/tutorial_info = - This is just a normal object, dropped in the room and setting the - mood. This is an easy solution, but in a real game one would probably - want to modify the look command to be able to see various 'scenery'- - like property on the room itself rather than creating faux - game-objects like this. -# -@create/drop The sea (in the distance);sea;ocean -# -@desc sea = - The gray sea stretches as far as the eye can see to the east. Far - below you its waves crash against the foot of the cliff. The vast - inland moor meets the ocean along a high and uninviting coastline of - ragged vertical stone. - - Once this part of the world might have been beautiful, but now the - eternal winds and storms have washed it all down into a gray and - barren wasteland. -# -@lock sea = get:false() -# -@set sea/get_err_msg = No one gets the sea. The sea gets you. -# -@set sea/tutorial_info = - This is just a normal object, dropped in the room and setting the - mood. This is an easy solution, but in a real game one would probably - want to modify the look command to be able to see various 'scenery'- - like property on the room itself rather than creating faux - game-objects like this. - + contents of the Attribute 'readable_sign', containing the information + on the sign. # Set a climbable object for discovering a hidden exit # @create/drop gnarled old trees;tree;trees;gnarled : tutorial_world.objects.Climbable @@ -317,15 +307,20 @@ start # @set trees/get_err_msg = The group of old trees have withstood the eternal wind for hundreds - of years. You will not uproot them any time soon. + of years. You will not uproot them any time soon. # @set trees/tutorial_info = These are climbable objects; they make for a small puzzle for accessing a hidden exit. Climbing the trees allows the - Climbable typeclass to assign a attribute on the character + Climbable typeclass to assign an Attribute on the character that an exit is then looking for. # -# The text to echo to player if trying 'climb tree' +# The text to echo to player if trying 'climb tree'. What +# happens when we do this is that the climb command assigns +# a Tag 'tutorial_climbed_tree' on the climber. The footpath +# exit (created below) is locked with this tag, meaning that +# it can only be seen/traversed by someone first having +# climbed. # @set tree/climb_text = With some effort you climb one of the old trees. @@ -334,10 +329,11 @@ start The branches are wet and slippery but can easily carry your weight. From this high vantage point you can see far and wide. - In fact, you notice {Ya faint yellowish light{n not far to the north, - beyond the trees. It looks like some sort of building. From this - angle you can make out a {wfaint footpath{n leading in that - direction, all but impossible to make out from ground level. + ... In fact, you notice {Ya faint yellowish light{n not far to the north, + beyond the trees. It looks like some sort of building. From this angle + you can make out a {wfaint footpath{n leading in that direction, all + but impossible to make out from ground level. You mentally register + where the footpath starts and will now be able to find it again. You climb down again. @@ -356,34 +352,48 @@ start = northern path;north;n;path,back to cliff;back;cliff;south;s # # Lock exit from view/traverse until we climbed that tree (which is -# when last_climbed get assigned). +# when tutorial_climbed_tree Tag gets assigned to us). # -@lock north = view:attr(last_climbed) ; traverse:attr(last_climbed) +@lock north = view:tag(tutorial_climbed_tree) ; traverse:tag(tutorial_climbed_tree) # @desc north = This is a hardly visible footpath leading off through the rain-beaten grass. It seems to circle the trees northward. You would never had - noticed it had you not seen it from above. + noticed it had you not spotted it from up in the tree. # @set north/tutorial_info = This exit is locked with a lock string that looks like this: - view:attr(last_climbed); traverse:attr(last_climbed) + view:tag(tutorial_climbed_tree) ; traverse:tag(tutorial_climbed_tree) - This checks if Character has an attribute last_climbed assigned in - order to be displayed and traversed. This attribute is set by the - trees when they are climbed. + This checks if Character has a Tag tutorial_climbed_tree set before it + allows itself to be displayed. This Tag is set by the tree object when + the 'climb' command is used. # -# Now that the exit is prepared, move to outside inn +# Now that the exit is prepared, move to outside inn to continue building. # north # @desc You stand outside a one-story sturdy wooden building. Light flickers behind closed storm shutters. Over the door a sign creaks in the wind - - the writing says {cEvennia Inn{n and is surrounded by a painted - image of some sort of snake. From inside you hear the sound of - laughter, singing and loud conversation. + - the writing says {cEvennia Inn{n and the curly letters are + surrounded by a painted image of some sort of snake. From inside you + hear the sound of laughter, singing and loud conversation. +# +# Some details to look at +# +@detail shutters;storm = + The shutters are closed. +# +@detail evennia = + You think you might have heard of this name before, + but at the moment you can recall where from. +# +@detail snake;letters = + The snake is cartoonish with big googly eyes. It looks somewhat + like one of those big snakes from the distant jungles - the kind + squeezes their victims. #------------------------------------------------------------ # @@ -408,46 +418,39 @@ north sticking out. There is a sign next to it: {wFree to take{n. A patron tells you cheerfully that it's the leftovers from those foolish adventurers that challenged the old ruin before you ... + + (to get a weapon from the barrel, use {wget weapon{n) +# +@detail barkeep;man;landlord = + The landlord is a cheerful fellow, always ready to supply you with + more beer. He mentions doing some sort of arcane magic known as + "software development" when not running this place. Whatever that + means. # @set here/tutorial_info = Nothing special about this room, only a bonus place to potentially go for chatting with other online players. Oh, and don't forget to grab - a blade if you don't already have one. The weapons are locked so that - you cannot take more than one. (only three blades are available in - this location and they won't get refilled until a player goes to the - outro room, so the barrel might be empty - if it's any comfort, the - weapons in there won't help much against what is waiting in the ruin - anyway ...) + a blade if you don't already have one. # -# Create the weapons to be held by the barrel. +# Create the weapon rack (the barrel) # -@create/drop rusty old sword;rusty;sword;weapon : tutorial_world.objects.Weapon +@create/drop barrel: tutorial_world.objects.WeaponRack # -@desc rusty = - This is a rusty old broadsword. It has seen better days but the hilt is in good shape. +@lock barrel = get:false() # -# Only allow to pick up if we don't already has something called weapon -@lock rusty = get:not holds(weapon) +# This id makes sure that we cannot pick more than one weapon from this rack # -@set rusty/get_err_msg = "The barkeep smiles and says: 'Now, don't be greedy, friend! You already have a weapon.'" +@set barrel/rack_id = "rack_barrel" # -@create/drop blunt axe;blunt;axe;weapon : tutorial_world.objects.Weapon +# Set which weapons are available from this rack. These are prototype-keys +# defined in tutorial_world.objects.WEAPON_PROTOTYPES. We also set a +# message to use when trying to grab a second weapon. # -@desc axe = - A heavy weapon, but the edge is dull and covered in rust and nicks. +@set barrel/available_weapons = ["knife", "rusty_dagger", "sword", "club", "ornate_longsword"] # -@lock blunt = get:not holds(weapon) -# -@set blunt/get_err_msg = "The barkeep crosses his arms: 'Hey, you already have a weapon; no need for another!'" -# -@create/drop patched spear;patched;spear;weapon : tutorial_world.objects.Weapon -# -@desc patched = - The spear tip looks to be in decent condition, but the shaft was broken and is rather poorly mended. -# -@lock patched = get:not holds(weapon) -# -@set patched/get_err_msg = "The barkeep shakes his head: 'You already have a weapon, friend, leave some for the next poor sod.'" +@set barrel/no_more_weapons_msg = + The barkeep shakes his head. He says: 'Sorry pal. We get a lot of needy + adventurers coming through here. One weapon per person only.' # #------------------------------------------------------------ # @@ -458,11 +461,11 @@ north # Back to cliff @teleport tut#02 # -# The bridge uses parent rooms.BridgeRoom, which causes the player to -# take a longer time than expected to cross as they are pummeled by -# wind and a chance to fall off. This room should not have regular -# exits back to the cliff, that is handled by the bridge typeclass -# itself. +# The bridge uses parent tutorial_world.rooms.BridgeRoom, which causes +# the player to take a longer time than expected to cross as they are +# pummeled by wind and a chance to fall off. This room should not have +# regular exits back to the cliff, that is handled by the bridge +# typeclass itself. # @dig The old bridge;bridge;tut#05 : tutorial_world.rooms.BridgeRoom @@ -472,7 +475,8 @@ north # @desc bridge = The hanging bridge's foundation sits at the edge of the cliff to the - east - two heavy stone pillars anchor the bridge on this side. + east - two heavy stone pillars anchor the bridge on this side. The + bridge sways precariously in the storm. # # go to the bridge # @@ -484,18 +488,19 @@ bridge # @set here/west_exit = tut#02 # -# connect other end to gatehouse +# connect other end to gatehouse (we have not created it yet +# but we know it will have alias tut#09 according to our map) # @set here/east_exit = tut#09 # -# Fall location is the cliff ledge +# Fall location is the cliff ledge (created next) # @set here/fall_exit = tut#06 # @set here/tutorial_info = - The bridge is a single room that uses a custom cmdset to overrule the - movement commands. This makes it take a few steps to cross it despite - it being only one room. + All of the bridge is actually a single room that uses a custom cmdset + to overrule the movement commands. This makes it take several steps to + cross it despite it being only one room in the database. The bridge has no normal exits, instead it has a counter that tracks @@ -518,56 +523,42 @@ bridge #------------------------------------------------------------ # # You only end up at the ledge if you fall off the bridge. It -# has no direct connection to the bridge. +# has no direct connection to the bridge but we specified +# it as the target of the "fall_exit", which is a special +# feature of the BridgeRoom. # @dig/teleport Protruding ledge;cliffledge;ledge;tut#06 # @set here/tutorial_info = - This room is stored as an attribute on the 'Bridge' room and used as a - destination should the player fall off the bridge. In our example the - bridge is relatively simple and always drops us to the same ledge; a - more advanced implementation might implement different locations to - end up in depending on what happens on the bridge. + This room is stored as an attribute on the 'Bridge' room and used as + a destination should the player fall off the bridge. It is the only + way to get to this room. In our example the bridge is relatively + simple and always drops us to the same ledge; a more advanced + implementation might implement different locations to end up in + depending on what happens on the bridge. # @desc You are on a narrow ledge protruding from the side of the cliff, about halfway down. The air is saturated with salty sea water, - sprays hitting your face from the crashing waves below. + sprays hitting your face from the crashing waves {wbelow{n. The ledge is covered with a few black-grey brushes. Not far from you the cliff-face is broken down to reveal a narrow natural opening into - the cliff. + the cliff. High above you the {wbridge{n sways and creaks in the wind. # -@create/drop brushes;brush -# -@lock brush = get:false() -# -@desc brush = +@detail brush;brushes = The brushes covering the ledge are gray and dwarfed from constantly being pummeled by salt, rain and wind. # -@create/drop The sea (far below you);sea;ocean +@detail below;sea;ocean;waves = + Below you the gray sea rages, its waves crashing into the cliff so a + thin mist of salt mixes with the rain even this far above it. You can + almost imagine the cliff trembling under its onslaught. # -@set sea/get_err_msg: - Try as you might, the Sea will always get you long before you can - ever get it. -# -@lock sea = get:false() -# -@desc sea = - Below you the gray sea rages. You can almost imagine the - cliff trembling under its onslaught. -# -@create/drop The hang bridge (above you);bridge;hangbridge;above -# -@lock bridge = get:false() -# -@desc bridge = - You can see the shape of the hanging bridge a fair bit above you, partly - obscured by the rain. There is no way to get back up there from this +@detail bridge = + Partly obscured by the rain you can make out the shape of the hanging + bridge high above you. There is no way to get back up there from this ledge. -# -@set bridge/get_err_msg = You can't reach it, it's too far away. #------------------------------------------------------------ @@ -615,30 +606,23 @@ hole simmers down the hole together with rain that ripples the black surface of the pool. # -@create/drop pool;water -# -@lock pool = get:false() -# -@set pool/get_err_msg = - You sift your hands through the black water without feeling any - immediate bottom. It's chilling cold and so dark you don't feel like - taking a sip. -# -@desc pool = +@detail pool;water = The water of the pool is black and opaque. The rain coming down from above does not seem to ripple the surface quite as much as it should. # -@create/drop hole (high above);hole;above +@detail bucket = + The bucket is nearly coming apart, only rusty iron bands holding + the rotten wood together. It's been down here for a long time. # -@lock hole = get:false() -# -@set hole/get_err_msg = You cannot reach it from here. You need to climb the chain. -# -@desc hole = +@detail hole;above = Whereas the lower edges of the hole seem jagged and natural you can faintly make out that it turns into a man-made circular shaft higher up. It looks like an old well. # +@detail passages;dark = + Those dark passages seem to criss-cross the cliff. No need to + head back into the gloom now that there seems to be a way out. +# # From the passages we get back up to the cliff, so we # open up a new exit back there. # @@ -684,20 +668,12 @@ hole thick cover of black roots having broken through the cracks from the outside.{n # -@create/drop iron-cast door;iron;door;iron-cast -# -@lock door = get:false() -# -@desc door = +@detail iron-cast door;iron;door;iron-cast = The door is very solid and clad in iron. No matter how much you push at it, it won't budge. It actually doesn't show any signs of having been opened for a very long time. # -@create/drop stone walls;walls;stone -# -@lock stone = get:false() -# -@desc stone = +@detail stone walls;walls;stone;stones;wall = The walls are dripping with moisture and mold. A network of roots have burst through the cracks on one side, bending the stones slightly aside. You feel a faint draft from that direction. @@ -705,9 +681,9 @@ hole # The crumbling wall is in fact an advanced type of Exit, all we need to do is # to supply it with a destination. # -# Puzzle wall is an exit without a given destination at start @create/drop root-covered wall;wall;roots;wines;root : tutorial_world.objects.CrumblingWall # +# # This destination is auto-assigned to the exit when its puzzle is solved # connect to the Underground passages @set root-covered wall/destination = tut#07 @@ -730,11 +706,12 @@ hole # head back up to ground level. We teleport to the bridge # and continue from there. # -# Back to the bridge @teleport tut#05 # # The bridge room should not have any normal exits from it, that is -# handled by the bridge itself. So we teleport away from it. +# handled by the bridge itself. So we teleport away from it. The +# ruined gatehouse is also the east_exit target for the bridge as +# we recall. # @dig/teleport Ruined gatehouse;gatehouse;tut#09 : tutorial_world.rooms.TutorialRoom @@ -762,17 +739,14 @@ hole the remains of the castle. There is also a standing archway offering passage to a path along the old {wsouth{nern inner wall. # -@create/drop fallen portoculis;portoculis;fall;fallen -# -@lock portoculis = get:false() -# -@desc portoculis = +@detail portoculis;fall;fallen;grating This heavy iron grating used to block off the inner part of the gate house, now it has fallen to the ground together with the stone archway that once help it up. # -# We lock the bridge exit for the mob, so it don't wander out on the bridge +# We lock the bridge exit for the mob, so it don't wander out on the bridge. Only +# traversing objects controlled by a player (i.e. Characters) may cross the bridge. # -@lock bridge = traverse:not attr(is_mob) +@lock bridge = traverse:has_player() #------------------------------------------------------------ # @@ -823,7 +797,7 @@ archway # @desc The ruins opens up to the sky in a small open area, lined by - columns. The open area is dominated by a huge stone obelisk in its + columns. The open area is dominated by a huge stone {wobelisk{n in its center, an ancient ornament miraculously still standing. Previously one could probably continue past the obelisk and eastward @@ -849,18 +823,18 @@ archway # # (the obelisk describes itself, so we need no do it here) # -# Create the mobile. This is the start location. -@create/drop Ghostly apparition;ghost;apparition;fog : tutorial_world.mob.Enemy +# Create the mobile. This is its start location. +@create/drop Ghostly apparition;ghost;apparition;fog : tutorial_world.mob.Mob # -@set ghost/full_health = 20 +# Set its home to this location # -@set ghost/defeat_location = dark cell +@home ghost = tut#11 # @lock ghost = get:false() # @set ghost/get_err_msg = Your fingers just pass straight through it! # -@desc ghost = +@desc ghost/desc_alive = This ghostly shape could momentarily be mistaken for a thick fog had it not moved with such determination and giving echoing hollow screams as it did. The shape is hard to determine, now and then it @@ -868,33 +842,69 @@ archway later. The thing reeks of almost tangible spite at your presence. This must be the ruin's eternal guardian. # +@desc ghost/desc_dead = + The ghostly apparition is nothing but a howling on the wind, an eternal + cold spot that can never be fully eradicated from these walls. While harmless + in this state, there is no doubt that it shall eventually return to this plane + to continue its endless haunting. +# +# We set the ghost to send defeated enemies to the Dark Cell +# +@set ghost/send_defeated_to = tut#08 +# +@set ghost/defeat_msg = + You fall to the ground, defeated. As you do, the ghostly apparition dives + forward and engulf you. + + + The world turns black. +# +@set ghost/defeat_msg_room = + %s falls to the ground, defeated. For a moment their fallen form is + engulfed by the swirling mists of the ghostly apparition. When they + raise lift, the ground is empty! +# +@set ghost/weapon_ineffective_msg = + Your weapon just passes through the swirling mist of the ghostly apparition, causing no effect! +# +@set ghost/hit_msg = + The ghostly apparition howls and writhes, shifts and shivers. +@set ghost/death_msg = + After the last strike, the ghostly apparition seems to collapse +inwards. It fades and becomes one with the mist. Its howls rise to a +ear-shattering crescendo before quickly fading away to be nothing more +than the lonely cries of the cold, salty wind. +# # Give the enemy some random echoes (echoed at irregular intervals) -# This 'list structure' [ ... ] is parsed by the batch reader and -# split by commas (so each entry cannot contain commas). # -@set ghost/irregular_echoes = - ["The foggy thing gives off a high-pitched shriek.","For a moment the - fog wraps around a nearby pillar.", "The fog drifts lower to the ground - as if looking for something.", "The fog momentarily takes on a reddish - hue.", "The fog temporarily fills most of the area as it changes - shape.", "You accidentally breathes in some of the fog - you start - coughing from the cold moisture."] +@set ghost/irregular_msgs = + ["The foggy thing gives off a high-pitched shriek.", + "For a moment the fog wraps around a nearby pillar.", + "The fog drifts lower to the ground as if looking for something.", + "The fog momentarily takes on a reddish hue.", + "The fog temporarily fills most of the area as it changes shape.", + "You accidentally breathes in some of the fog - you start coughing from the cold moisture."] # + # give the enemy a tentacle weapon # @create foggy tentacles;tentacles:tutorial_world.objects.Weapon # -# Make the enemy's weapon good - hits at 70% of attacks +# Make the enemy's weapon good - hits at 70% of attacks, but not good at parrying. # @set foggy tentacles/hit = 0.7 # +@set foggy tentacles/parry = 0.1 +# +@set foggy tentacles/damage = 5 +# # Actually give the enemy its weapon # @teleport/quiet tentacles = ghost # -# Clear inactive mode and start the mob +# Start the mob # -@set ghost/inactive = +mobon ghost #------------------------------------------------------------ # @@ -930,11 +940,7 @@ archway withstood the test of time better than many of those around it, it looks like some sort of temple. # -@create/drop old stables;stable;stables;building -# -@lock stable = get:false() -# -@desc stable = +@detail stables;stable;building = The building is empty, if it was indeed once a stable it was abandoned long ago. #------------------------------------------------------------ @@ -960,6 +966,12 @@ archway Stairs lead down to the temple's dungeon on either side of the altar. A gaping door opening shows the a wide courtyard to the west. +# +@detail altar = + The altar is a massive stone slab. It might once have had ornate decorations + but time and the salty air has broken everything down into dust. +@detail ceiling = + The dome still looming intact above you is a marvel of engineering. #------------------------------------------------------------ # @@ -975,7 +987,7 @@ archway The stairs are worn by the age-old passage of feet. # # Lock the antechamber so the ghost cannot get in there. -@lock stairs down = traverse:not attr(is_mob) +@lock stairs down = traverse:has_player() # # Go down # @@ -1004,87 +1016,21 @@ stairs down directly relay you on to the Dark Cell. If correct, the tomb teleports to the Ancient Tomb treasure chamber. # -# We create all the tombs +# We create all the tombs. These all teleport to the dark cell +# except one which is the one decided by the scene shown by the +# Obelisk last we looked. # -@dig Blue bird tomb +@dig/tel Blue bird tomb : tutorial_world.rooms.TeleportRoom = Tomb with stone bird;bird;blue;stone # -@dig Tomb of woman on horse - : tutorial_world.rooms.TeleportRoom - = Tomb with statue of riding woman;horse;riding; -# -@dig Tomb of the crowned queen - : tutorial_world.rooms.TeleportRoom - = Tomb with statue of a crowned queen;crown;queen -# -@dig Tomb of the shield - : tutorial_world.rooms.TeleportRoom - = Tomb with shield of arms;shield -# -@dig Tomb of the hero - : tutorial_world.rooms.TeleportRoom - = Tomb depicting a heroine fighting a monster;knight;hero;monster;beast -# -# The puzzle_values are set on Character by looking at the Obelisk in -# the Castle Corner room. If the scenes shown don't match, the -# failure/success_teleport_to attributes will be used to teleport away -# the Character. Since the scene shown by the Obelisk is random, this -# means the right tomb need not be the same. -# -@tel Blue bird tomb -# @set here/puzzle_value = 0 # -@set here/failure_teleport_to = falling! +@set here/failure_teleport_to = tut#15 # -@set here/success_teleport_to = Ancient tomb +@set here/success_teleport_to = tut#16 # -@teleport Tomb of woman on horse -# -@set here/puzzle_value = 1 -# -@set here/failure_teleport_to = falling! -# -@set here/success_teleport_to = Ancient tomb -# -@teleport Tomb of the crowned queen -# -@set here/puzzle_value = 2 -# -@set here/failure_teleport_to = falling! -# -@set here/success_teleport_to = Ancient tomb -# -@teleport Tomb of the shield -# -@set here/puzzle_value = 3 -# -@set here/failure_teleport_to = falling! -# -@set here/success_teleport_to = Ancient tomb -# -@teleport Tomb of the hero -# -@set here/puzzle_value = 4 -# -@set here/failure_teleport_to = falling! -# -@set here/success_teleport_to = Ancient tomb - -#------------------------------------------------------------ -# -# Falling room -# -# This is a transition between the trap and the cell room. Character -# is teleported here if they picked the wrong tomb. -# -#------------------------------------------------------------ -# -@dig/teleport Falling!;falling;tut#15 - : tutorial_world.rooms.TeleportRoom -# -@desc +@set here/failure_teleport_to = The tomb is dark. You fumble your way through it. You think you can make out a coffin in front of you in the gloom. @@ -1102,9 +1048,162 @@ stairs down The air is damp. Where are you? # -@set here/success_teleport_to = dark cell +@set here/success_teleport_to = + The tomb is dark. You fumble your way through it. You think you can + make out a coffin in front of you in the gloom. + + The coffin comes into view. On and around it are chiseled scenes of a + stern woman in armor. They depict great heroic deeds. This is clearly + the tomb of some sort of ancient heroine - it must be the goal you + have been looking for! # -@set here/failure_teleport_to = dark cell +@dig Tomb of woman on horse + : tutorial_world.rooms.TeleportRoom + = Tomb with statue of riding woman;horse;riding; +# +@set here/puzzle_value = 1 +# +@set here/failure_teleport_to = tut#15 +# +@set here/success_teleport_to = tut#16 +# +@set here/failure_teleport_to = + The tomb is dark. You fumble your way through it. You think you can + make out a coffin in front of you in the gloom. + + + {rSuddenly you hear a distinct 'click' and the ground abruptly + disappears under your feet! You fall ... things go dark. {n + + + ... + + + ... You come to your senses. You lie down. On stone floor. You + shakily come to your feet. Somehow you suspect that you are not under + the tomb anymore, like you were magically snatched away. + + The air is damp. Where are you? +# +@set here/success_teleport_to = + The tomb is dark. You fumble your way through it. You think you can + make out a coffin in front of you in the gloom. + + The coffin comes into view. On and around it are chiseled scenes of a + stern woman in armor. They depict great heroic deeds. This is clearly + the tomb of some sort of ancient heroine - it must be the goal you + have been looking for! +# +@dig Tomb of the crowned queen + : tutorial_world.rooms.TeleportRoom + = Tomb with statue of a crowned queen;crown;queen +# +@set here/puzzle_value = 2 +# +@set here/failure_teleport_to = tut#15 +# +@set here/success_teleport_to = tut#16 +# +@set here/failure_teleport_to = + The tomb is dark. You fumble your way through it. You think you can + make out a coffin in front of you in the gloom. + + + {rSuddenly you hear a distinct 'click' and the ground abruptly + disappears under your feet! You fall ... things go dark. {n + + + ... + + + ... You come to your senses. You lie down. On stone floor. You + shakily come to your feet. Somehow you suspect that you are not under + the tomb anymore, like you were magically snatched away. + + The air is damp. Where are you? +# +@set here/success_teleport_to = + The tomb is dark. You fumble your way through it. You think you can + make out a coffin in front of you in the gloom. + + The coffin comes into view. On and around it are chiseled scenes of a + stern woman in armor. They depict great heroic deeds. This is clearly + the tomb of some sort of ancient heroine - it must be the goal you + have been looking for! +# +@dig Tomb of the shield + : tutorial_world.rooms.TeleportRoom + = Tomb with shield of arms;shield +# +@set here/puzzle_value = 3 +# +@set here/failure_teleport_to = tut#15 +# +@set here/success_teleport_to = tut#16 +# +@set here/failure_teleport_to = + The tomb is dark. You fumble your way through it. You think you can + make out a coffin in front of you in the gloom. + + + {rSuddenly you hear a distinct 'click' and the ground abruptly + disappears under your feet! You fall ... things go dark. {n + + + ... + + + ... You come to your senses. You lie down. On stone floor. You + shakily come to your feet. Somehow you suspect that you are not under + the tomb anymore, like you were magically snatched away. + + The air is damp. Where are you? +# +@set here/success_teleport_to = + The tomb is dark. You fumble your way through it. You think you can + make out a coffin in front of you in the gloom. + + The coffin comes into view. On and around it are chiseled scenes of a + stern woman in armor. They depict great heroic deeds. This is clearly + the tomb of some sort of ancient heroine - it must be the goal you + have been looking for! +# +@dig Tomb of the hero + : tutorial_world.rooms.TeleportRoom + = Tomb depicting a heroine fighting a monster;knight;hero;monster;beast +# +@set here/puzzle_value = 4 +# +@set here/failure_teleport_to = tut#15 +# +@set here/success_teleport_to = tut#16 +# +@set here/failure_teleport_to = + The tomb is dark. You fumble your way through it. You think you can + make out a coffin in front of you in the gloom. + + + {rSuddenly you hear a distinct 'click' and the ground abruptly + disappears under your feet! You fall ... things go dark. {n + + + ... + + + ... You come to your senses. You lie down. On stone floor. You + shakily come to your feet. Somehow you suspect that you are not under + the tomb anymore, like you were magically snatched away. + + The air is damp. Where are you? +# +@set here/success_teleport_to = + The tomb is dark. You fumble your way through it. You think you can + make out a coffin in front of you in the gloom. + + The coffin comes into view. On and around it are chiseled scenes of a + stern woman in armor. They depict great heroic deeds. This is clearly + the tomb of some sort of ancient heroine - it must be the goal you + have been looking for! # # back to antechamber @tel tut#14 @@ -1123,13 +1222,9 @@ stairs down = ,back to antechamber;antechamber;back # @desc - The tomb is dark. You fumble your way through it. You think you can - make out a coffin in front of you in the gloom. - - The coffin comes into view. On and around it are chiseled scenes of a - stern woman in armor. They depict great heroic deeds. This is clearly - the tomb of some sort of ancient heroine - it must be the goal you - have been looking for! + Apart from the ornate sarcophagus, the tomb is bare from extra decorations. + This is the resting place of a warrior with little patience for + glamour and trinkets. # @set here/tutorial_info = Congratulations, you have reached the end of this little tutorial @@ -1140,25 +1235,23 @@ stairs down explore further. You will find this weapon works better against the castle's guardian than any of the others you have found ... # -# The exact nature of the weapon is randomized. The get_text attribute -# on the sarcophagus holds a %s replacement that is filled by the typeclass -# with the name of the weapon when you get the weapon. +# The sarcophagus is a "weapon rack" from which you can extract one +# single weapon. # @create/drop Stone sarcophagus;sarcophagus;stone : tutorial_world.objects.WeaponRack # @desc stone = The lid of the coffin is adorned with a stone statue in full size. The weapon held by - the stone hands looks very realistic ... + the stone hands looks very realistic ... ( + + (try {wget weapon{n) # @set sarcophagus/rack_id = rack_sarcophagus # -@set sarcophagus/min_dmg = 4.0 +@set sarcophagus/no_more_weapons_msg = + The tomb has already granted you all the might it will ever do. # -@set sarcophagus/max_dmg = 11.0 -# -@set sarcophagus/magic = True -# -@set sarcophagus/get_text = +@set sarcophagus/get_weapon_msg = The hands of the statue close on what seems to be a real weapon rather than one in stone. This must be the hero's legendary weapon! The prize you have been looking for! @@ -1197,9 +1290,9 @@ stairs down @tel tut#17 # # we want to clear the weapon-rack ids on the character when exiting. -@set here/wracklist = ["rack_sarcophagus"] +@set here/wracklist = ["rack_barrel", "rack_sarcophagus"] # -# this quits the tutorial and cleans up all variables that was . +# this room quits the tutorial and cleans up all variables that were set. @desc {gThanks for trying out this little Evennia tutorial! @@ -1213,14 +1306,15 @@ stairs down If you went through the tutorial quest once, it can be interesting to do it again to explore the various possibilities and rooms you might - not have come across yet, maybe with the source/build code to one - side. If you play as superuser (user #1) the mobile will ignore you - and teleport rooms etc will not affect you (this will also disable - all locks, so keep that in mind when checking functionality).{n + not have come across yet, maybe with the source/build code next to + you. If you play as superuser (user #1) the mobile will ignore you + and teleport rooms etc will not affect you (this will also disable all + locks, so keep that in mind when checking functionality).{n # @set here/tutorial_info = - This room cleans up all temporary attributes that was put on the - character during the tutorial. Hope you enjoyed the play through! + This room cleans up all temporary attributes and tags that were put + on the character during the tutorial. Hope you enjoyed the play + through! # # Tie this back to Limbo # diff --git a/evennia/contrib/tutorial_world/mob.py b/evennia/contrib/tutorial_world/mob.py index 6f380e6590..82fdf0c2fc 100644 --- a/evennia/contrib/tutorial_world/mob.py +++ b/evennia/contrib/tutorial_world/mob.py @@ -9,13 +9,55 @@ import random from evennia import TICKER_HANDLER from evennia import search_object +from evennia import Command, CmdSet from evennia.contrib.tutorial_world import objects as tut_objects +class CmdMobOnOff(Command): + """ + Activates/deactivates Mob + + Usage: + mobon + moboff + + This turns the mob from active (alive) mode + to inactive (dead) mode. It is used during + building to activate the mob once it's + prepared. + """ + key = "mobon" + aliases = "moboff" + locks = "cmd:superuser()" + + def func(self): + """ + Uses the mob's set_alive/set_dead methods + to turn on/off the mob." + """ + if not self.args: + self.caller.msg("Usage: mobon|moboff ") + return + mob = self.caller.search(self.args) + if not mob: + return + if self.cmdname == "mobon": + mob.set_alive() + else: + mob.set_dead() + + +class MobCmdSet(CmdSet): + """ + Holds the admin command controlling the mob + """ + def at_cmdset_creation(self): + self.add(CmdMobOnOff()) + class Mob(tut_objects.TutorialObject): """ - This is a state-machine AI mobile. It has several states which - are controlled from setting various Attributes: + This is a state-machine AI mobile. It has several states which are + controlled from setting various Attributes. All default to True: patrolling: if set, the mob will move randomly from room to room, but preferring to not return @@ -30,18 +72,23 @@ class Mob(tut_objects.TutorialObject): to flee from it, so it can enter combat. If unset, it will return to patrolling/idling if fled from. immortal: If set, the mob cannot take any damage. - It also has several states, - is_patrolling - set when the mob is patrolling. - is_attacking - set when the mob is in combat - is_hunting - set when the mob is pursuing an enemy. - is_immortal - is currently immortal - is_dead: if set, the Mob is set to immortal, non-patrolling - and non-aggressive mode. Its description is - turned into that of a corpse-description. - Other important properties: - home - the home location should set to someplace inside - the patrolling area. The mob will use this if it should - happen to roam into a room with no exits. + irregular_echoes: list of strings the mob generates at irregular intervals. + desc_alive: the physical description while alive + desc_dead: the physical descripion while dead + send_defeated_to: unique key/alias for location to send defeated enemies to + defeat_msg: message to echo to defeated opponent + defeat_msg_room: message to echo to room. Accepts %s as the name of the defeated. + hit_msg: message to echo when this mob is hit. Accepts %s for the mob's key. + weapon_ineffective_msg: message to echo for useless attacks + death_msg: message to echo to room when this mob dies. + patrolling_pace: how many seconds per tick, when patrolling + aggressive_pace: -"- attacking + hunting_pace: -"- hunting + death_pace: -"- returning to life when dead + + field 'home' - the home location should set to someplace inside + the patrolling area. The mob will use this if it should + happen to roam into a room with no exits. """ def __init__(self): @@ -60,10 +107,11 @@ class Mob(tut_objects.TutorialObject): Called the first time the object is created. We set up the base properties and flags here. """ + self.cmdsets.add(MobCmdSet, permanent=True) # Main AI flags. We start in dead mode so we don't have to # chase the mob around when building. - self.db.patrolling = False - self.db.aggressive = False + self.db.patrolling = True + self.db.aggressive = True self.db.immortal = True # db-store if it is dead or not self.db.is_dead = True @@ -99,10 +147,11 @@ class Mob(tut_objects.TutorialObject): # text to echo to the defeated foe. self.db.defeat_msg = "You fall to the ground." self.db.defeat_msg_room = "%s falls to the ground." - self.db.weapon_ineffective_text = "Your weapon just passes through your enemy, causing almost no effect!" + self.db.weapon_ineffective_msg = "Your weapon just passes through your enemy, causing almost no effect!" self.db.death_msg = "After the last hit %s evaporates." % self.key self.db.hit_msg = "%s wails, shudders and writhes." % self.key + self.db.irregular_msgs = ["the enemy looks about.", "the enemy changes stance."] self.db.tutorial_info = "This is an object with simple state AI, using a ticker to move." @@ -127,7 +176,7 @@ class Mob(tut_objects.TutorialObject): previous ticker subscription so that we can easily find and stop it before setting a new one. The tickerhandler is persistent so - we need to remmeber this across reloads. + we need to remember this across reloads. """ idstring = "tutorial_mob" # this doesn't change @@ -155,7 +204,7 @@ class Mob(tut_objects.TutorialObject): if obj.has_player and not obj.is_superuser] return targets[0] if targets else None - def set_alive(self): + def set_alive(self, *args, **kwargs): """ Set the mob to "alive" mode. This effectively resurrects it from the dead state. @@ -232,7 +281,7 @@ class Mob(tut_objects.TutorialObject): self.ndb.is_hunting = False self.ndb.is_attacking = True - def do_patrol(self): + def do_patrol(self, *args, **kwargs): """ Called repeatedly during patrolling mode. In this mode, the mob scans its surroundings and randomly chooses a viable exit. @@ -240,6 +289,8 @@ class Mob(tut_objects.TutorialObject): order to block the mob from moving outside its area while allowing player-controlled characters to move normally. """ + if random.random() < 0.01: + self.location.msg_contents(random.choice(self.db.irregular_msgs)) if self.db.aggressive: # first check if there are any targets in the room. target = self._find_target(self.location) @@ -263,12 +314,14 @@ class Mob(tut_objects.TutorialObject): # no exits! teleport to home to get away. self.move_to(self.home) - def do_hunting(self): + def do_hunting(self, *args, **kwargs): """ Called regularly when in hunting mode. In hunting mode the mob scans adjacent rooms for enemies and moves towards them to attack if possible. """ + if random.random() < 0.01: + self.location.msg_contents(random.choice(self.db.irregular_msgs)) if self.db.aggressive: # first check if there are any targets in the room. target = self._find_target(self.location) @@ -293,12 +346,14 @@ class Mob(tut_objects.TutorialObject): # no exits! teleport to home to get away. self.move_to(self.home) - def do_attacking(self): + def do_attacking(self, *args, **kwargs): """ Called regularly when in attacking mode. In attacking mode the mob will bring its weapons to bear on any targets in the room. """ + if random.random() < 0.01: + self.location.msg_contents(random.choice(self.db.irregular_msgs)) # first make sure we have a target target = self._find_target(self.location) if not target: diff --git a/evennia/contrib/tutorial_world/objects.py b/evennia/contrib/tutorial_world/objects.py index 0b6bbc0326..392c5d09c8 100644 --- a/evennia/contrib/tutorial_world/objects.py +++ b/evennia/contrib/tutorial_world/objects.py @@ -19,11 +19,9 @@ WeaponRack """ -import time import random -from evennia import create_object -from evennia import DefaultObject, DefaultExit, Command, CmdSet, DefaultScript +from evennia import DefaultObject, DefaultExit, Command, CmdSet from evennia import utils from evennia.utils.spawner import spawn @@ -171,8 +169,8 @@ 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 + # set a tag on the caller to remember that we climbed. + self.caller.tags.add("tutorial_climbed_tree") class CmdSetClimbable(CmdSet): @@ -611,8 +609,6 @@ class CrumblingWall(TutorialObject, DefaultExit): # 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) @@ -671,7 +667,8 @@ class CrumblingWall(TutorialObject, DefaultExit): # 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" + "crisscross the wall, making it hard to clearly see its stony surface. Maybe you could " \ + "try to {wshift{n or {wmove{n them.\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) @@ -851,7 +848,7 @@ class Weapon(TutorialObject): super(Weapon, self).at_object_creation() self.db.hit = 0.4 # hit chance self.db.parry = 0.8 # parry chance - self.db.damage = 8.0 + self.db.damage = 1.0 self.db.magic = False self.cmdset.add_default(CmdSetWeapon, permanent=True) @@ -1007,6 +1004,13 @@ class WeaponRack(TutorialObject): 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 players + who already got one weapon from the rack and tries to + grab another one. + """ def at_object_creation(self): """ @@ -1016,21 +1020,27 @@ class WeaponRack(TutorialObject): self.db.rack_id = "weaponrack_1" # these are prototype names from the prototype # dictionary above. + self.db.get_weapon_msg = "You pull %s from the rack." + self.db.no_more_weapons_msg = "%s has no more to offer you." % self.key self.db.available_weapons = ["knife", "rusty_dagger", "sword", "club"] def produce_weapon(self, caller): """ This will produce a new weapon from the rack, - assuming the caller hasn't already gotten one. + 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. """ - if caller.attributes.get(self.db.rack_id): + rack_id = self.db.rack_id + if caller.tags.get(rack_id): caller.msg("%s has no more to offer you." % self.key) else: prototype = random.choice(self.db.available_weapons) # use the spawner to create a new Weapon from the - # spawner dictionary + # spawner dictionary, tag the caller wpn = spawn(WEAPON_PROTOTYPES[prototype], prototype_parents=WEAPON_PROTOTYPES) - caller.attributes.add(self.db.rack_id, True) + caller.tags.add(rack_id) wpn.location = caller - caller.msg("You grab %s - %s." % (wpn.key, wpn.db.desc)) + caller.msg(self.db.weapon_msg % wpn.key) diff --git a/evennia/contrib/tutorial_world/rooms.py b/evennia/contrib/tutorial_world/rooms.py index 903a7be8a1..3666b08d0c 100644 --- a/evennia/contrib/tutorial_world/rooms.py +++ b/evennia/contrib/tutorial_world/rooms.py @@ -16,6 +16,11 @@ from evennia import utils, create_object, search_object from evennia import syscmdkeys, default_cmds from evennia.contrib.tutorial_world.objects import LightSource, TutorialObject +# the system error-handling module is defined in the settings. We load the +# given setting here using utils.object_from_module. This way we can use +# it regardless of if we change settings later. +from django.conf import settings +_SEARCH_AT_RESULT = utils.object_from_module(settings.SEARCH_AT_RESULT) #------------------------------------------------------------ # @@ -68,15 +73,139 @@ class CmdTutorial(Command): caller.msg("{RSorry, there is no tutorial help available here.{n") +# for the @detail command we inherit from MuxCommand, since +# we want to make use of MuxCommand's pre-parsing of '=' in the +# argument. +class CmdTutorialSetDetail(default_cmds.MuxCommand): + """ + sets a detail on a room + + Usage: + @detail = + @detail ;;... = description + + Example: + @detail walls = The walls are covered in ... + @detail castle;ruin;tower = The distant ruin ... + + This sets a "detail" on the object this command is defined on + (TutorialRoom for this tutorial). This detail can be accessed with + the TutorialRoomLook command sitting on TutorialRoom objects (details + are set as a simple dictionary on the room). This is a Builder command. + + We custom parse the key for the ;-separator in order to create + multiple aliases to the detail all at once. + """ + key = "@detail" + locks = "cmd:perm(Builders)" + help_category = "TutorialWorld" + + def func(self): + """ + All this does is to check if the object has + the set_detail method and uses it. + """ + if not self.args or not self.rhs: + self.caller.msg("Usage: @detail key = description") + return + if not hasattr(self.obj, "set_detail"): + self.caller.msg("Details cannot be set on %s." % self.obj) + return + for key in self.args.split(";"): + # loop over all aliases, if any (if not, this will just be + # the one key to loop over) + self.obj.set_detail(key, self.rhs) + self.caller.msg("Detail set: '%s': '%s'" % (self.lhs, self.rhs)) + + +class CmdTutorialLook(default_cmds.CmdLook): + """ + looks at the room and on details + + Usage: + look + look + look * + + Observes your location, details at your location or objects + in your vicinity. + + Tutorial: This is a child of the default Look command, that also + allows us to look at "details" in the room. These details are + things to examine and offers some extra description without + actually having to be actual database objects. It uses the + return_detail() hook on TutorialRooms for this. + """ + # we don't need to specify key/locks etc, this is already + # set by the parent. + help_category = "TutorialWorld" + + def func(self): + """ + Handle the looking. This is a copy of the default look + code except for adding in the details. + """ + caller = self.caller + args = self.args + print "tutorial look" + if args: + # we use quiet=True to turn off automatic error reporting. + # This tells search that we want to handle error messages + # ourself. This also means the search function will always + # return a list (with 0, 1 or more elements) rather than + # result/None. + looking_at_obj = caller.search(args, use_nicks=True, quiet=True) + if len(looking_at_obj) != 1: + # no target found or more than one target found (multimatch) + # look for a detail that may match + detail = self.obj.return_detail(args) + print "look detail:", detail, self.obj, self.obj.db.details + if detail: + self.caller.msg(detail) + return + else: + # no detail found, delegate our result to the normal + # error message handler. + _SEARCH_AT_RESULT(caller, args, looking_at_obj) + return + else: + # we found a match, extract it from the list and carry on + # normally with the look handling. + looking_at_obj = looking_at_obj[0] + + else: + looking_at_obj = caller.location + if not looking_at_obj: + caller.msg("You have no location to look at!") + return + + if not hasattr(looking_at_obj, 'return_appearance'): + # this is likely due to us having a player instead + looking_at_obj = looking_at_obj.character + if not looking_at_obj.access(caller, "view"): + caller.msg("Could not find '%s'." % args) + return + # get object's appearance + caller.msg(looking_at_obj.return_appearance(caller)) + # the object's at_desc() method. + looking_at_obj.at_desc(looker=caller) + + + class TutorialRoomCmdSet(CmdSet): """ - Implements the simple tutorial cmdset + Implements the simple tutorial cmdset. This will overload the look + command in the default CharacterCmdSet since it has a higher + priority (ChracterCmdSet has prio 0) """ key = "tutorial_cmdset" + priority = 1 def at_cmdset_creation(self): - "add the tutorial cmd" + "add the tutorial-room commands" self.add(CmdTutorial()) + self.add(CmdTutorialSetDetail()) + self.add(CmdTutorialLook()) class TutorialRoom(DefaultRoom): @@ -95,6 +224,11 @@ class TutorialRoom(DefaultRoom): the room about it by trying to call a hook on them. The Mob object uses this to cheaply get notified of enemies without having to constantly scan for them. + + Args: + new_arrival (Object): the object that just entered this room. + source_location (Object): the previous location of new_arrival. + """ if new_arrival.has_player and not new_arrival.is_superuser: # this is a character @@ -102,6 +236,36 @@ class TutorialRoom(DefaultRoom): if hasattr(obj, "at_new_arrival"): obj.at_new_arrival(new_arrival) + def return_detail(self, detailkey): + """ + This looks for an Attribute "obj_details" and possibly + returns the value of it. + + Args: + detailkey (str): The detail being looked at + + """ + details = self.db.details + if details: + return details.get(detailkey, None) + + def set_detail(self, detailkey, description): + """ + This sets a new detail, using an Attribute "details". + + Args: + detailkey (str): The detail identifier to add (for + aliases you need to add multiple keys to the + same description). + description (str): The text to return when looking + at the given detailkey. + + """ + if self.db.details: + self.db.details[detailkey] = description + else: + self.db.details = {detailkey: description} + def reset(self): "Can be called by the tutorial runner." pass @@ -155,11 +319,13 @@ class WeatherRoom(TutorialRoom): self.db.tutorial_info = \ "This room has a Script running that has it echo a weather-related message at irregular intervals." - def update_weather(self): + def update_weather(self, *args, **kwargs): """ Called by the tickerhandler at regular intervals. Even so, we only update 20% of the time, picking a random weather message - when we do. + when we do. The tickerhandler requires that this hook accepts + any arguments and keyword arguments (hence the *args, **kwargs + even though we don't actually use them in this example) """ if random.random() < 0.2: # only update 20 % of the time @@ -168,7 +334,7 @@ class WeatherRoom(TutorialRoom): #------------------------------------------------------------------------------ # -# Dark Room - a scripted room +# Dark Room - a room with states # # This room limits the movemenets of its denizens unless they carry an active # LightSource object (LightSource is defined in @@ -395,7 +561,10 @@ class TeleportRoom(TutorialRoom): Important attributes (set at creation): puzzle_key - which attr to look for on character puzzle_value - what char.db.puzzle_key must be set to - teleport_to - where to teleport to in case of failure to match + success_teleport_to - where to teleport in case if success + success_teleport_msg - message to echo while teleporting to success + failure_teleport_to - where to teleport to in case of failure + failure_teleport_msg - message to echo while teleporting to failure """ def at_object_creation(self): @@ -405,8 +574,10 @@ class TeleportRoom(TutorialRoom): self.db.puzzle_value = 1 # target of successful teleportation. Can be a dbref or a # unique room name. + self.db.success_teleport_msg = "You are successful!" self.db.success_teleport_to = "treasure room" # the target of the failure teleportation. + self.db.failure_teleport_msg = "You fail!" self.db.failure_teleport_to = "dark cell" def at_object_receive(self, character, source_location): @@ -417,14 +588,10 @@ class TeleportRoom(TutorialRoom): if not character.has_player: # only act on player characters. return - #print character.db.puzzle_clue, self.db.puzzle_value - if character.db.puzzle_clue != self.db.puzzle_value: - # we didn't pass the puzzle. See if we can teleport. - teleport_to = self.db.failure_teleport_to # this is a room name - else: - # passed the puzzle - teleport_to = self.db.success_teleport_to # this is a room name - + # determine if the puzzle is a success or not + is_success = character.db.puzzle_clue == self.db.puzzle_value + teleport_to = self.db.success_teleport_to if is_success else self.db.failure_teleport_to + # note that this returns a list results = search_object(teleport_to) if not results or len(results) > 1: # we cannot move anywhere since no valid target was found. @@ -434,8 +601,11 @@ class TeleportRoom(TutorialRoom): # superusers don't get teleported character.msg("Superuser block: You would have been teleported to %s." % results[0]) return - # the teleporter room's desc should give the 'teleporting message'. - character.execute_cmd("look") + # perform the teleport + if is_success: + character.msg(self.db.success_teleport_msg) + else: + character.msg(self.db.failure_teleport_msg) # teleport quietly to the new place character.move_to(results[0], quiet=True) @@ -468,7 +638,7 @@ class CmdEast(Command): when exiting east. - west_exit: a unique name or dbref to the room to go to when exiting west. - The room must also have the following property: + The room must also have the following Attributes - tutorial_bridge_posistion: the current position on on the bridge, 0 - 4. @@ -687,13 +857,8 @@ class BridgeRoom(WeatherRoom): self.db.fall_exit = "cliffledge" # add the cmdset on the room. self.cmdset.add_default(BridgeCmdSet) - # information for those using the tutorial command - self.db.tutorial_info = \ - "The bridge seems large but is actually only a " \ - "single room that assigns custom west/east commands " \ - "and a counter to determine how far across you are." - def update_weather(self): + def update_weather(self, *args, **kwargs): """ This is called at irregular intervals and makes the passage over the bridge a little more interesting. @@ -801,7 +966,7 @@ class OutroRoom(TutorialRoom): """ Called when the room is first created. """ - super(IntroRoom, self).at_object_creation() + super(OutroRoom, self).at_object_creation() self.db_tutorial_info = "The last room of the tutorial. " \ "This cleans up all temporary Attributes " \ "the tutorial may have assigned to the "\ diff --git a/evennia/locks/lockfuncs.py b/evennia/locks/lockfuncs.py index c910791475..caeb4b2ba3 100644 --- a/evennia/locks/lockfuncs.py +++ b/evennia/locks/lockfuncs.py @@ -422,6 +422,27 @@ def attr_ne(accessing_obj, accessed_obj, *args, **kwargs): """ return attr(accessing_obj, accessed_obj, *args, **{'compare': 'ne'}) +def tag(accessing_obj, accessed_obj, *args, **kwargs): + """ + Usage: + tag(tagkey) + tag(tagkey, category) + + Only true if accessing_obj has the specified tag and optional + category + """ + return accessing_obj.tags.get(*args) + +def objtag(accessing_obj, accessed_obj, *args, **kwargs): + """ + Usage: + objtag(tagkey) + objtag(tagkey, category) + + Only true if accessed_obj has the specified tag and optional + category. + """ + return accessed_obj.tags.get(*args) def inside(accessing_obj, accessed_obj, *args, **kwargs): """ diff --git a/evennia/scripts/tickerhandler.py b/evennia/scripts/tickerhandler.py index 58e7fc6ece..d50ee53198 100644 --- a/evennia/scripts/tickerhandler.py +++ b/evennia/scripts/tickerhandler.py @@ -268,7 +268,8 @@ class TickerHandler(object): the same time interval hook_key (str, optional): The name of the hook method on `obj` to call every `interval` seconds. Defaults to - `at_tick()`. + `at_tick(*args, **kwargs`. All hook methods must + always accept *args, **kwargs. args, kwargs (optional): These will be passed into the method given by `hook_key` every time it is called. diff --git a/evennia/utils/utils.py b/evennia/utils/utils.py index f2784df182..e6086a2c4f 100644 --- a/evennia/utils/utils.py +++ b/evennia/utils/utils.py @@ -16,7 +16,6 @@ import textwrap import datetime import random import traceback -from subprocess import check_output from importlib import import_module from inspect import ismodule, trace from collections import defaultdict @@ -967,7 +966,8 @@ def class_from_module(path, defaultpaths=None): err += "." raise ImportError(err) return cls - +# alias +object_from_module = class_from_module def init_new_player(player): """