diff --git a/evennia/commands/cmdhandler.py b/evennia/commands/cmdhandler.py index 7adef541dc..135235d8cd 100644 --- a/evennia/commands/cmdhandler.py +++ b/evennia/commands/cmdhandler.py @@ -171,8 +171,8 @@ def get_and_merge_cmdsets(caller, session, player, obj, # Gather all cmdsets stored on objects in the room and # also in the caller's inventory and the location itself local_objlist = yield (location.contents_get(exclude=obj) + - obj.contents + - [location]) + obj.contents + [location]) + local_objlist = [o for o in local_objlist if not o._is_deleted] for lobj in local_objlist: try: # call hook in case we need to do dynamic changing to cmdset @@ -205,6 +205,8 @@ def get_and_merge_cmdsets(caller, session, player, obj, yield obj.at_cmdset_get() except Exception: logger.log_trace() + _msg_err(caller, _ERROR_CMDSETS) + raise ErrorReported try: returnValue(obj.cmdset.current) except AttributeError: diff --git a/evennia/contrib/tutorial_world/build.ev b/evennia/contrib/tutorial_world/build.ev index 5ee23c5547..65eaa59b70 100644 --- a/evennia/contrib/tutorial_world/build.ev +++ b/evennia/contrib/tutorial_world/build.ev @@ -38,14 +38,14 @@ # | +--------+ +--------+ +--------+ +---+----+ # | \ | # ++---------+ \ +--------+ +--------+ +---+----+ -# |intro | \ |cell | |trap/ |temple | -# o--+ 01 | \| 08 +----+ fall | | 13 | -# | | | | /| 15 | | | +# |intro | \ |cell | | | |temple | +# o--+ 01 | \| 08 +----+ trap | | 13 | +# | | | | /| | | | # +----+-----+ +--------+ / +--+-+-+-+ +---+----+ # | / | | | | # +----+-----+ +--------+/ +--+-+-+---------+----+ # |outro | |tomb | |antechamber | -# o--+ 17 +----------+ 16 | | 14 | +# o--+ 16 +----------+ 15 | | 14 | # | | | | | | # +----------+ +--------+ +---------------------+ # @@ -826,8 +826,11 @@ archway # @set obelisk/get_err_msg = It's way too heavy for anyone to move. # -# (the obelisk describes itself, so we need no do it here) +# Set the puzzle clues on the obelisk. The order should correspond +# to the ids later checked by the antechamber puzzle. # +@set obelisk/puzzle_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 sharp-faced woman with white hair{n.") + # Create the mobile. This is its start location. @create/drop Ghostly apparition;ghost;apparition;fog : tutorial_world.mob.Mob # @@ -874,6 +877,7 @@ archway # @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 @@ -1028,7 +1032,7 @@ stairs down # @dig Blue bird tomb : tutorial_world.rooms.TeleportRoom - = Tomb with stone bird;bird;blue;stone + = Blue bird tomb;bird;blue;stone # @desc Blue bird tomb = The entrance to this tomb is decorated with a very lifelike blue bird. @@ -1037,9 +1041,9 @@ Blue bird tomb # @set here/puzzle_value = 0 # -@set here/failure_teleport_to = tut#15 +@set here/failure_teleport_to = tut#08 # -@set here/success_teleport_to = tut#16 +@set here/success_teleport_to = tut#15 # @set here/failure_teleport_msg = The tomb is dark. You fumble your way through it. You think you can @@ -1059,7 +1063,7 @@ Blue bird tomb The air is damp. Where are you? # -@set here/success_teleport_to = +@set here/success_teleport_msg = 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. @@ -1072,7 +1076,7 @@ Blue bird tomb # @dig Tomb of woman on horse : tutorial_world.rooms.TeleportRoom - = Tomb with statue of riding woman;horse;riding; + = Tomb of woman on horse;horse;riding; # @desc Tomb of woman on horse = The entrance to this tomb depicts a scene of a strong @@ -1084,9 +1088,9 @@ Tomb of woman on horse # @set here/puzzle_value = 1 # -@set here/failure_teleport_to = tut#15 +@set here/failure_teleport_to = tut#08 # -@set here/success_teleport_to = tut#16 +@set here/success_teleport_to = tut#15 # @set here/failure_teleport_msg = The tomb is dark. You fumble your way through it. You think you can @@ -1106,7 +1110,7 @@ Tomb of woman on horse The air is damp. Where are you? # -@set here/success_teleport_to = +@set here/success_teleport_msg = 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. @@ -1119,7 +1123,7 @@ Tomb of woman on horse # @dig Tomb of the crowned queen : tutorial_world.rooms.TeleportRoom - = Tomb with statue of a crowned queen;crown;queen + = Tomb of the crowned queen;crown;queen # @desc Tomb of the crowned queen = The entrance to this tomb shows a beautiful mural of a queen ruling @@ -1130,9 +1134,9 @@ Tomb of the crowned queen # @set here/puzzle_value = 2 # -@set here/failure_teleport_to = tut#15 +@set here/failure_teleport_to = tut#08 # -@set here/success_teleport_to = tut#16 +@set here/success_teleport_to = tut#15 # @set here/failure_teleport_msg = The tomb is dark. You fumble your way through it. You think you can @@ -1152,7 +1156,7 @@ Tomb of the crowned queen The air is damp. Where are you? # -@set here/success_teleport_to = +@set here/success_teleport_msg = 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. @@ -1165,9 +1169,7 @@ Tomb of the crowned queen # @dig Tomb of the shield : tutorial_world.rooms.TeleportRoom - = Tomb with shield of arms;shield -# -Tomb of the shield + = Tomb of the shield;shield # @desc Tomb of the shield = This tomb shows a warrior woman fighting shadowy creatures from the @@ -1175,11 +1177,13 @@ top of a hill. Her sword lies broken on the ground before her but she fights on with her battered shield - the scene depicts her just as she rams the shield into an enemy in wild desperation. # +Tomb of the shield +# @set here/puzzle_value = 3 # -@set here/failure_teleport_to = tut#15 +@set here/failure_teleport_to = tut#08 # -@set here/success_teleport_to = tut#16 +@set here/success_teleport_to = tut#15 # @set here/failure_teleport_msg = The tomb is dark. You fumble your way through it. You think you can @@ -1199,7 +1203,7 @@ rams the shield into an enemy in wild desperation. The air is damp. Where are you? # -@set here/success_teleport_to = +@set here/success_teleport_msg = 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. @@ -1212,7 +1216,7 @@ rams the shield into an enemy in wild desperation. # @dig Tomb of the hero : tutorial_world.rooms.TeleportRoom - = Tomb depicting a heroine fighting a monster;knight;hero;monster;beast + = Tomb of the hero;knight;hero;monster;beast # @desc Tomb of the hero = The entrance to this tomb shows a mural of an aging woman in a @@ -1225,9 +1229,9 @@ Tomb of the hero # @set here/puzzle_value = 4 # -@set here/failure_teleport_to = tut#15 +@set here/failure_teleport_to = tut#08 # -@set here/success_teleport_to = tut#16 +@set here/success_teleport_to = tut#15 # @set here/failure_teleport_msg = The tomb is dark. You fumble your way through it. You think you can @@ -1247,7 +1251,7 @@ Tomb of the hero The air is damp. Where are you? # -@set here/success_teleport_to = +@set here/success_teleport_msg = 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. @@ -1269,14 +1273,14 @@ Tomb of the hero # #------------------------------------------------------------ # -@dig/teleport Ancient tomb;tut#16 +@dig/teleport Ancient tomb;tut#15 : tutorial_world.rooms.TutorialRoom = ,back to antechamber;antechamber;back # @desc 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. + glamour and trinkets. You have reached the end of your quest. # @set here/tutorial_info = Congratulations, you have reached the end of this little tutorial @@ -1330,7 +1334,7 @@ Tomb of the hero # #------------------------------------------------------------ # -@dig End of tutorial;end;tut#17 +@dig End of tutorial;end;tut#16 : tutorial_world.rooms.OutroRoom = Exit tutorial;exit;end # @@ -1341,7 +1345,7 @@ Tomb of the hero @lock Exit tutorial = view:holds(rack_sarcophagus) ; traverse:holds(rack_sarcophagus) # # to tutorial outro -@tel tut#17 +@tel tut#16 # # we want to clear the weapon-rack ids on the character when exiting. @set here/wracklist = ["rack_barrel", "rack_sarcophagus"] diff --git a/evennia/contrib/tutorial_world/objects.py b/evennia/contrib/tutorial_world/objects.py index ac08d50a10..513b44d57f 100644 --- a/evennia/contrib/tutorial_world/objects.py +++ b/evennia/contrib/tutorial_world/objects.py @@ -52,7 +52,6 @@ class TutorialObject(DefaultObject): "Called when the object is first created." super(TutorialObject, self).at_object_creation() self.db.tutorial_info = "No tutorial info is available for this object." - #self.db.last_reset = time.time() def reset(self): "Resets the object, whatever that may mean." @@ -171,7 +170,7 @@ class CmdClimb(Command): ostring = "You climb %s. Having looked around, you climb down again." % self.obj.name self.caller.msg(ostring) # set a tag on the caller to remember that we climbed. - self.caller.tags.add("tutorial_climbed_tree") + self.caller.tags.add("tutorial_climbed_tree", category="tutorial_world") class CmdSetClimbable(CmdSet): @@ -205,21 +204,22 @@ class Climbable(TutorialObject): # #------------------------------------------------------------ -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.") - class Obelisk(TutorialObject): """ - This object changes its description randomly. + This object changes its description randomly, and which is shown + determines which order "clue id" is stored on the Character for + future puzzles. + + Important Attribute: + puzzle_descs (list): list of descriptions. One of these is + """ def at_object_creation(self): "Called when object is created." super(Obelisk, self).at_object_creation() self.db.tutorial_info = "This object changes its desc randomly, and makes sure to remember which one you saw." + self.db.puzzle_descs = ["You see a normal stone slab"] # make sure this can never be picked up self.locks.add("get:false()") @@ -229,11 +229,12 @@ class Obelisk(TutorialObject): 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) + descs = self.db.puzzle_descs + clueindex = random.randint(0, len(descs) - 1) # set this description, with the random extra string = "The surface of the obelisk seem to waver, shift and writhe under your gaze, with " \ "different scenes and structures appearing whenever you look at it. " - self.db.desc = string + OBELISK_DESCS[clueindex] + self.db.desc = string + descs[clueindex] # remember that this was the clue we got. The Puzzle room will # look for this later to determine if you should be teleported # or not. @@ -1051,13 +1052,13 @@ class WeaponRack(TutorialObject): pulling weapons from it indefinitely. """ rack_id = self.db.rack_id - if caller.tags.get(rack_id): + if caller.tags.get(rack_id, category="tutorial_world"): caller.msg(self.db.no_more_weapons_msg) else: prototype = random.choice(self.db.available_weapons) # use the spawner to create a new Weapon from the # spawner dictionary, tag the caller wpn = spawn(WEAPON_PROTOTYPES[prototype], prototype_parents=WEAPON_PROTOTYPES)[0] - caller.tags.add(rack_id) + caller.tags.add(rack_id, category="tutorial_world") wpn.location = caller caller.msg(self.db.get_weapon_msg % wpn.key) diff --git a/evennia/contrib/tutorial_world/rooms.py b/evennia/contrib/tutorial_world/rooms.py index 314b3aa36a..10d4f3495c 100644 --- a/evennia/contrib/tutorial_world/rooms.py +++ b/evennia/contrib/tutorial_world/rooms.py @@ -265,10 +265,6 @@ class TutorialRoom(DefaultRoom): else: self.db.details = {detailkey.lower(): description} - def reset(self): - "Can be called by the tutorial runner." - pass - #------------------------------------------------------------ # @@ -331,6 +327,347 @@ class WeatherRoom(TutorialRoom): self.msg_contents("{w%s{n" % random.choice(WEATHER_STRINGS)) + +SUPERUSER_WARNING = "\nWARNING: You are playing as a superuser ({name}). Use the {quell} command to\n" \ + "play without superuser privileges (many functions and puzzles ignore the \n" \ + "presence of a superuser, making this mode useful for exploring things behind \n" \ + "the scenes later).\n" \ + + +#----------------------------------------------------------- +# +# Intro Room - unique room +# +# This room marks the start of the tutorial. It sets up properties on +# the player char that is needed for the tutorial. +# +#------------------------------------------------------------ + +class IntroRoom(TutorialRoom): + """ + Intro room + + properties to customize: + char_health - integer > 0 (default 20) + """ + def at_object_creation(self): + """ + Called when the room is first created. + """ + super(IntroRoom, self).at_object_creation() + self.db_tutorial_info = "The first room of the tutorial. " \ + "This assigns the health Attribute to "\ + "the player." + + def at_object_receive(self, character, source_location): + """ + Assign properties on characters + """ + + # setup character for the tutorial + health = self.db.char_health or 20 + + if character.has_player: + character.db.health = health + character.db.health_max = health + + if character.is_superuser: + string = "-"*78 + SUPERUSER_WARNING + "-"*78 + character.msg("{r%s{n" % string.format(name=character.key, quell="{w@quell{r")) + + +#------------------------------------------------------------ +# +# Bridge - unique room +# +# Defines a special west-eastward "bridge"-room, a large room it takes +# several steps to cross. It is complete with custom commands and a +# chance of falling off the bridge. This room has no regular exits, +# instead the exiting are handled by custom commands set on the player +# upon first entering the room. +# +# Since one can enter the bridge room from both ends, it is +# divided into five steps: +# westroom <- 0 1 2 3 4 -> eastroom +# +#------------------------------------------------------------ + + +class CmdEast(Command): + """ + Go eastwards across the bridge. + + Tutorial info: + This command relies on the caller having two Attributes + (assigned by the room when entering): + - east_exit: a unique name or dbref to the room to go to + 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 Attributes + - tutorial_bridge_posistion: the current position on + on the bridge, 0 - 4. + + """ + key = "east" + aliases = ["e"] + locks = "cmd:all()" + help_category = "TutorialWorld" + + def func(self): + "move one step eastwards" + caller = self.caller + + bridge_step = min(5, caller.db.tutorial_bridge_position + 1) + + if bridge_step > 4: + # we have reached the far east end of the bridge. + # Move to the east room. + eexit = search_object(self.obj.db.east_exit) + if eexit: + caller.move_to(eexit[0]) + else: + caller.msg("No east exit was found for this room. Contact an admin.") + return + caller.db.tutorial_bridge_position = bridge_step + # since we are really in one room, we have to notify others + # in the room when we move. + caller.location.msg_contents("%s steps eastwards across the bridge." % caller.name, exclude=caller) + caller.execute_cmd("look") + + +# go back across the bridge +class CmdWest(Command): + """ + Go westwards across the bridge. + + Tutorial info: + This command relies on the caller having two Attributes + (assigned by the room when entering): + - east_exit: a unique name or dbref to the room to go to + 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: + - tutorial_bridge_posistion: the current position on + on the bridge, 0 - 4. + + """ + key = "west" + aliases = ["w"] + locks = "cmd:all()" + help_category = "TutorialWorld" + + def func(self): + "move one step westwards" + caller = self.caller + + bridge_step = max(-1, caller.db.tutorial_bridge_position - 1) + + if bridge_step < 0: + # we have reached the far west end of the bridge. + # Move to the west room. + wexit = search_object(self.obj.db.west_exit) + if wexit: + caller.move_to(wexit[0]) + else: + caller.msg("No west exit was found for this room. Contact an admin.") + return + caller.db.tutorial_bridge_position = bridge_step + # since we are really in one room, we have to notify others + # in the room when we move. + caller.location.msg_contents("%s steps westwards across the bridge." % caller.name, exclude=caller) + caller.execute_cmd("look") + + +BRIDGE_POS_MESSAGES = ("You are standing {wvery close to the the bridge's western foundation{n. If you go west you will be back on solid ground ...", + "The bridge slopes precariously where it extends eastwards towards the lowest point - the center point of the hang bridge.", + "You are {whalfways{n out on the unstable bridge.", + "The bridge slopes precariously where it extends westwards towards the lowest point - the center point of the hang bridge.", + "You are standing {wvery close to the bridge's eastern foundation{n. If you go east you will be back on solid ground ...") +BRIDGE_MOODS = ("The bridge sways in the wind.", "The hanging bridge creaks dangerously.", + "You clasp the ropes firmly as the bridge sways and creaks under you.", + "From the castle you hear a distant howling sound, like that of a large dog or other beast.", + "The bridge creaks under your feet. Those planks does not seem very sturdy.", + "Far below you the ocean roars and throws its waves against the cliff, as if trying its best to reach you.", + "Parts of the bridge come loose behind you, falling into the chasm far below!", + "A gust of wind causes the bridge to sway precariously.", + "Under your feet a plank comes loose, tumbling down. For a moment you dangle over the abyss ...", + "The section of rope you hold onto crumble in your hands, parts of it breaking apart. You sway trying to regain balance.") + +FALL_MESSAGE = "Suddenly the plank you stand on gives way under your feet! You fall!" \ + "\nYou try to grab hold of an adjoining plank, but all you manage to do is to " \ + "divert your fall westwards, towards the cliff face. This is going to hurt ... " \ + "\n ... The world goes dark ...\n\n" \ + + +class CmdLookBridge(Command): + """ + looks around at the bridge. + + Tutorial info: + This command assumes that the room has an Attribute + "fall_exit", a unique name or dbref to the place they end upp + if they fall off the bridge. + """ + key = 'look' + aliases = ["l"] + locks = "cmd:all()" + help_category = "TutorialWorld" + + def func(self): + "Looking around, including a chance to fall." + caller = self.caller + bridge_position = self.caller.db.tutorial_bridge_position + # this command is defined on the room, so we get it through self.obj + location = self.obj + # randomize the look-echo + message = "{c%s{n\n%s\n%s" % (location.key, + BRIDGE_POS_MESSAGES[bridge_position], + random.choice(BRIDGE_MOODS)) + + chars = [obj for obj in self.obj.contents_get(exclude=caller) if obj.has_player] + if chars: + # we create the You see: message manually here + message += "\n You see: %s" % ", ".join("{c%s{n" % char.key for char in chars) + self.caller.msg(message) + + # there is a chance that we fall if we are on the western or central + # part of the bridge. + if bridge_position < 3 and random.random() < 0.05 and not self.caller.is_superuser: + # we fall 5% of time. + fall_exit = search_object(self.obj.db.fall_exit) + if fall_exit: + self.caller.msg("{r%s{n" % FALL_MESSAGE) + self.caller.move_to(fall_exit[0], quiet=True) + # inform others on the bridge + self.obj.msg_contents("A plank gives way under %s's feet and " \ + "they fall from the bridge!" % self.caller.key) + + +# custom help command +class CmdBridgeHelp(Command): + """ + Overwritten help command while on the bridge. + """ + key = "help" + aliases = ["h"] + locks = "cmd:all()" + help_category = "Tutorial world" + + def func(self): + "Implements the command." + string = "You are trying hard not to fall off the bridge ..." + string += "\n\nWhat you can do is trying to cross the bridge {weast{n " + string += "or try to get back to the mainland {wwest{n)." + self.caller.msg(string) + + +class BridgeCmdSet(CmdSet): + "This groups the bridge commands. We will store it on the room." + key = "Bridge commands" + priority = 1 # this gives it precedence over the normal look/help commands. + def at_cmdset_creation(self): + "Called at first cmdset creation" + self.add(CmdTutorial()) + self.add(CmdEast()) + self.add(CmdWest()) + self.add(CmdLookBridge()) + self.add(CmdBridgeHelp()) + + +BRIDGE_WEATHER = ( + "The rain intensifies, making the planks of the bridge even more slippery.", + "A gush of wind throws the rain right in your face.", + "The rainfall eases a bit and the sky momentarily brightens.", + "The bridge shakes under the thunder of a closeby thunder strike.", + "The rain pummels you with large, heavy drops. You hear the distinct howl of a large hound in the distance.", + "The wind is picking up, howling around you and causing the bridge to sway from side to side.", + "Some sort of large bird sweeps by overhead, giving off an eery screech. Soon it has disappeared in the gloom.", + "The bridge sways from side to side in the wind.", + "Below you a particularly large wave crashes into the rocks.", + "From the ruin you hear a distant, otherwordly howl. Or maybe it was just the wind.") + + +class BridgeRoom(WeatherRoom): + """ + The bridge room implements an unsafe bridge. It also enters the player into + a state where they get new commands so as to try to cross the bridge. + + We want this to result in the player getting a special set of + commands related to crossing the bridge. The result is that it + will take several steps to cross it, despite it being represented + by only a single room. + + We divide the bridge into steps: + + self.db.west_exit - - | - - self.db.east_exit + 0 1 2 3 4 + + The position is handled by a variable stored on the character + when entering and giving special move commands will + increase/decrease the counter until the bridge is crossed. + + We also has self.db.fall_exit, which points to a gathering + location to end up if we happen to fall off the bridge (used by + the CmdLookBridge command). + + """ + def at_object_creation(self): + "Setups the room" + # this will start the weather room's ticker and tell + # it to call update_weather regularly. + super(BridgeRoom, self).at_object_creation() + # this identifies the exits from the room (should be the command + # needed to leave through that exit). These are defaults, but you + # could of course also change them after the room has been created. + self.db.west_exit = "cliff" + self.db.east_exit = "gate" + self.db.fall_exit = "cliffledge" + # add the cmdset on the room. + self.cmdset.add_default(BridgeCmdSet) + + def update_weather(self, *args, **kwargs): + """ + This is called at irregular intervals and makes the passage + over the bridge a little more interesting. + """ + if random.random() < 80: + # send a message most of the time + self.msg_contents("{w%s{n" % random.choice(BRIDGE_WEATHER)) + + def at_object_receive(self, character, source_location): + """ + This hook is called by the engine whenever the player is moved + into this room. + """ + if character.has_player: + # we only run this if the entered object is indeed a player object. + # check so our east/west exits are correctly defined. + wexit = search_object(self.db.west_exit) + eexit = search_object(self.db.east_exit) + fexit = search_object(self.db.fall_exit) + if not (wexit and eexit and fexit): + character.msg("The bridge's exits are not properly configured. "\ + "Contact an admin. Forcing west-end placement.") + character.db.tutorial_bridge_position = 0 + return + if source_location == eexit[0]: + # we assume we enter from the same room we will exit to + character.db.tutorial_bridge_position = 4 + else: + # if not from the east, then from the west! + character.db.tutorial_bridge_position = 0 + + def at_object_leave(self, character, target_location): + """ + This is triggered when the player leaves the bridge room. + """ + if character.has_player: + # clean up the position attribute + del character.db.tutorial_bridge_position + + #------------------------------------------------------------------------------ # # Dark Room - a room with states @@ -341,6 +678,7 @@ class WeatherRoom(TutorialRoom): # #------------------------------------------------------------------------------ + DARK_MESSAGES = ("It is pitch black. You are likely to be eaten by a grue.", "It's pitch black. You fumble around but cannot find anything.", "You don't see a thing. You feel around, managing to bump your fingers hard against something. Ouch!", @@ -357,6 +695,7 @@ ALREADY_LIGHTSOURCE = "You don't want to stumble around in blindness anymore. Yo FOUND_LIGHTSOURCE = "Your fingers bump against a splinter of wood in a corner. It smells of resin and seems dry enough to burn! " \ "You pick it up, holding it firmly. Now you just need to {wlight{n it using the flint and steel you carry with you." + class CmdLookDark(Command): """ Look around in darkness @@ -542,9 +881,10 @@ class DarkRoom(TutorialRoom): """ self.check_light_state() + #------------------------------------------------------------ # -# Teleport room - puzzle room +# Teleport room - puzzles solution # # This is a sort of puzzle room that requires a certain # attribute on the entering character to be the same as @@ -557,6 +897,7 @@ class DarkRoom(TutorialRoom): # #------------------------------------------------------------ + class TeleportRoom(TutorialRoom): """ Teleporter - puzzle room. @@ -592,7 +933,7 @@ class TeleportRoom(TutorialRoom): # only act on player characters. return # determine if the puzzle is a success or not - is_success = character.db.puzzle_clue == self.db.puzzle_value + is_success = str(character.db.puzzle_clue) == str(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) @@ -600,7 +941,7 @@ class TeleportRoom(TutorialRoom): # we cannot move anywhere since no valid target was found. print "no valid teleport target for %s was found." % teleport_to return - if character.player.is_superuser: + if character.is_superuser: # superusers don't get teleported character.msg("Superuser block: You would have been teleported to %s." % results[0]) return @@ -610,347 +951,12 @@ class TeleportRoom(TutorialRoom): else: character.msg(self.db.failure_teleport_msg) # teleport quietly to the new place - character.move_to(results[0], quiet=True) + character.move_to(results[0], quiet=True, move_hooks=False) #------------------------------------------------------------ # -# Bridge - unique room -# -# Defines a special west-eastward "bridge"-room, a large room it takes -# several steps to cross. It is complete with custom commands and a -# chance of falling off the bridge. This room has no regular exits, -# instead the exiting are handled by custom commands set on the player -# upon first entering the room. -# -# Since one can enter the bridge room from both ends, it is -# divided into five steps: -# westroom <- 0 1 2 3 4 -> eastroom -# -#------------------------------------------------------------ - - -class CmdEast(Command): - """ - Go eastwards across the bridge. - - Tutorial info: - This command relies on the caller having two Attributes - (assigned by the room when entering): - - east_exit: a unique name or dbref to the room to go to - 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 Attributes - - tutorial_bridge_posistion: the current position on - on the bridge, 0 - 4. - - """ - key = "east" - aliases = ["e"] - locks = "cmd:all()" - help_category = "TutorialWorld" - - def func(self): - "move one step eastwards" - caller = self.caller - - bridge_step = min(5, caller.db.tutorial_bridge_position + 1) - - if bridge_step > 4: - # we have reached the far east end of the bridge. - # Move to the east room. - eexit = search_object(self.obj.db.east_exit) - if eexit: - caller.move_to(eexit[0]) - else: - caller.msg("No east exit was found for this room. Contact an admin.") - return - caller.db.tutorial_bridge_position = bridge_step - # since we are really in one room, we have to notify others - # in the room when we move. - caller.location.msg_contents("%s steps eastwards across the bridge." % caller.name, exclude=caller) - caller.execute_cmd("look") - - -# go back across the bridge -class CmdWest(Command): - """ - Go westwards across the bridge. - - Tutorial info: - This command relies on the caller having two Attributes - (assigned by the room when entering): - - east_exit: a unique name or dbref to the room to go to - 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: - - tutorial_bridge_posistion: the current position on - on the bridge, 0 - 4. - - """ - key = "west" - aliases = ["w"] - locks = "cmd:all()" - help_category = "TutorialWorld" - - def func(self): - "move one step westwards" - caller = self.caller - - bridge_step = max(-1, caller.db.tutorial_bridge_position - 1) - - if bridge_step < 0: - # we have reached the far west end of the bridge. - # Move to the west room. - wexit = search_object(self.obj.db.west_exit) - if wexit: - caller.move_to(wexit[0]) - else: - caller.msg("No west exit was found for this room. Contact an admin.") - return - caller.db.tutorial_bridge_position = bridge_step - # since we are really in one room, we have to notify others - # in the room when we move. - caller.location.msg_contents("%s steps westwards across the bridge." % caller.name, exclude=caller) - caller.execute_cmd("look") - - -BRIDGE_POS_MESSAGES = ("You are standing {wvery close to the the bridge's western foundation{n. If you go west you will be back on solid ground ...", - "The bridge slopes precariously where it extends eastwards towards the lowest point - the center point of the hang bridge.", - "You are {whalfways{n out on the unstable bridge.", - "The bridge slopes precariously where it extends westwards towards the lowest point - the center point of the hang bridge.", - "You are standing {wvery close to the bridge's eastern foundation{n. If you go east you will be back on solid ground ...") -BRIDGE_MOODS = ("The bridge sways in the wind.", "The hanging bridge creaks dangerously.", - "You clasp the ropes firmly as the bridge sways and creaks under you.", - "From the castle you hear a distant howling sound, like that of a large dog or other beast.", - "The bridge creaks under your feet. Those planks does not seem very sturdy.", - "Far below you the ocean roars and throws its waves against the cliff, as if trying its best to reach you.", - "Parts of the bridge come loose behind you, falling into the chasm far below!", - "A gust of wind causes the bridge to sway precariously.", - "Under your feet a plank comes loose, tumbling down. For a moment you dangle over the abyss ...", - "The section of rope you hold onto crumble in your hands, parts of it breaking apart. You sway trying to regain balance.") - -FALL_MESSAGE = "Suddenly the plank you stand on gives way under your feet! You fall!" \ - "\nYou try to grab hold of an adjoining plank, but all you manage to do is to " \ - "divert your fall westwards, towards the cliff face. This is going to hurt ... " \ - "\n ... The world goes dark ...\n\n" \ - -class CmdLookBridge(Command): - """ - looks around at the bridge. - - Tutorial info: - This command assumes that the room has an Attribute - "fall_exit", a unique name or dbref to the place they end upp - if they fall off the bridge. - """ - key = 'look' - aliases = ["l"] - locks = "cmd:all()" - help_category = "TutorialWorld" - - def func(self): - "Looking around, including a chance to fall." - caller = self.caller - bridge_position = self.caller.db.tutorial_bridge_position - # this command is defined on the room, so we get it through self.obj - location = self.obj - # randomize the look-echo - message = "{c%s{n\n%s\n%s" % (location.key, - BRIDGE_POS_MESSAGES[bridge_position], - random.choice(BRIDGE_MOODS)) - - chars = [obj for obj in self.obj.contents_get(exclude=caller) if obj.has_player] - if chars: - # we create the You see: message manually here - message += "\n You see: %s" % ", ".join("{c%s{n" % char.key for char in chars) - self.caller.msg(message) - - # there is a chance that we fall if we are on the western or central - # part of the bridge. - if bridge_position < 3 and random.random() < 0.05 and not self.caller.is_superuser: - # we fall 5% of time. - fall_exit = search_object(self.obj.db.fall_exit) - if fall_exit: - self.caller.msg("{r%s{n" % FALL_MESSAGE) - self.caller.move_to(fall_exit[0], quiet=True) - # inform others on the bridge - self.obj.msg_contents("A plank gives way under %s's feet and " \ - "they fall from the bridge!" % self.caller.key) - - -# custom help command -class CmdBridgeHelp(Command): - """ - Overwritten help command while on the bridge. - """ - key = "help" - aliases = ["h"] - locks = "cmd:all()" - help_category = "Tutorial world" - - def func(self): - "Implements the command." - string = "You are trying hard not to fall off the bridge ..." - string += "\n\nWhat you can do is trying to cross the bridge {weast{n " - string += "or try to get back to the mainland {wwest{n)." - self.caller.msg(string) - - -class BridgeCmdSet(CmdSet): - "This groups the bridge commands. We will store it on the room." - key = "Bridge commands" - priority = 1 # this gives it precedence over the normal look/help commands. - def at_cmdset_creation(self): - "Called at first cmdset creation" - self.add(CmdTutorial()) - self.add(CmdEast()) - self.add(CmdWest()) - self.add(CmdLookBridge()) - self.add(CmdBridgeHelp()) - -BRIDGE_WEATHER = ( - "The rain intensifies, making the planks of the bridge even more slippery.", - "A gush of wind throws the rain right in your face.", - "The rainfall eases a bit and the sky momentarily brightens.", - "The bridge shakes under the thunder of a closeby thunder strike.", - "The rain pummels you with large, heavy drops. You hear the distinct howl of a large hound in the distance.", - "The wind is picking up, howling around you and causing the bridge to sway from side to side.", - "Some sort of large bird sweeps by overhead, giving off an eery screech. Soon it has disappeared in the gloom.", - "The bridge sways from side to side in the wind.", - "Below you a particularly large wave crashes into the rocks.", - "From the ruin you hear a distant, otherwordly howl. Or maybe it was just the wind.") - -class BridgeRoom(WeatherRoom): - """ - The bridge room implements an unsafe bridge. It also enters the player into - a state where they get new commands so as to try to cross the bridge. - - We want this to result in the player getting a special set of - commands related to crossing the bridge. The result is that it - will take several steps to cross it, despite it being represented - by only a single room. - - We divide the bridge into steps: - - self.db.west_exit - - | - - self.db.east_exit - 0 1 2 3 4 - - The position is handled by a variable stored on the character - when entering and giving special move commands will - increase/decrease the counter until the bridge is crossed. - - We also has self.db.fall_exit, which points to a gathering - location to end up if we happen to fall off the bridge (used by - the CmdLookBridge command). - - """ - def at_object_creation(self): - "Setups the room" - # this will start the weather room's ticker and tell - # it to call update_weather regularly. - super(BridgeRoom, self).at_object_creation() - # this identifies the exits from the room (should be the command - # needed to leave through that exit). These are defaults, but you - # could of course also change them after the room has been created. - self.db.west_exit = "cliff" - self.db.east_exit = "gate" - self.db.fall_exit = "cliffledge" - # add the cmdset on the room. - self.cmdset.add_default(BridgeCmdSet) - - def update_weather(self, *args, **kwargs): - """ - This is called at irregular intervals and makes the passage - over the bridge a little more interesting. - """ - if random.random() < 80: - # send a message most of the time - self.msg_contents("{w%s{n" % random.choice(BRIDGE_WEATHER)) - - def at_object_receive(self, character, source_location): - """ - This hook is called by the engine whenever the player is moved - into this room. - """ - if character.has_player: - # we only run this if the entered object is indeed a player object. - # check so our east/west exits are correctly defined. - wexit = search_object(self.db.west_exit) - eexit = search_object(self.db.east_exit) - fexit = search_object(self.db.fall_exit) - if not (wexit and eexit and fexit): - character.msg("The bridge's exits are not properly configured. "\ - "Contact an admin. Forcing west-end placement.") - character.db.tutorial_bridge_position = 0 - return - if source_location == eexit[0]: - # we assume we enter from the same room we will exit to - character.db.tutorial_bridge_position = 4 - else: - # if not from the east, then from the west! - character.db.tutorial_bridge_position = 0 - - def at_object_leave(self, character, target_location): - """ - This is triggered when the player leaves the bridge room. - """ - if character.has_player: - # clean up the position attribute - del character.db.tutorial_bridge_position - -SUPERUSER_WARNING = "\nWARNING: You are playing as a superuser ({name}). Use the {quell} command to\n" \ - "play without superuser privileges (many functions and puzzles ignore the \n" \ - "presence of a superuser, making this mode useful for exploring things behind \n" \ - "the scenes later).\n" \ - -#----------------------------------------------------------- -# -# Intro Room - unique room -# -# This room marks the start of the tutorial. It sets up properties on -# the player char that is needed for the tutorial. -# -#------------------------------------------------------------ - -class IntroRoom(TutorialRoom): - """ - Intro room - - properties to customize: - char_health - integer > 0 (default 20) - """ - def at_object_creation(self): - """ - Called when the room is first created. - """ - super(IntroRoom, self).at_object_creation() - self.db_tutorial_info = "The first room of the tutorial. " \ - "This assigns the health Attribute to "\ - "the player." - - def at_object_receive(self, character, source_location): - """ - Assign properties on characters - """ - - # setup character for the tutorial - health = self.db.char_health or 20 - - if character.has_player: - character.db.health = health - character.db.health_max = health - - if character.is_superuser: - string = "-"*78 + SUPERUSER_WARNING + "-"*78 - character.msg("{r%s{n" % string.format(name=character.key, quell="{w@quell{r")) - - -#------------------------------------------------------------ -# -# Outro room - unique room +# Outro room - unique exit room # # Cleans up the character from all tutorial-related properties. # @@ -989,6 +995,4 @@ class OutroRoom(TutorialRoom): del character.db.puzzle_clue del character.db.combat_parry_mode del character.db.tutorial_bridge_position - for tut_obj in [obj for obj in character.contents - if utils.inherits_from(obj, TutorialObject)]: - tut_obj.reset() + character.tags.clear(category="tutorial_world") diff --git a/evennia/objects/objects.py b/evennia/objects/objects.py index 651bb74e29..8b0909c1a3 100644 --- a/evennia/objects/objects.py +++ b/evennia/objects/objects.py @@ -557,7 +557,7 @@ class DefaultObject(ObjectDB): obj.msg(message, from_obj=from_obj, **kwargs) def move_to(self, destination, quiet=False, - emit_to_obj=None, use_destination=True, to_none=False): + emit_to_obj=None, use_destination=True, to_none=False, move_hooks=True): """ Moves this object to a new location. @@ -568,23 +568,26 @@ class DefaultObject(ObjectDB): function, such things are assumed to have been handled before calling move_to. - destination: (Object) Reference to the object to move to. This - can also be an exit object, in which case the destination - property is used as destination. - quiet: (bool) If true, don't emit left/arrived messages. - emit_to_obj: (Object) object to receive error messages - use_destination (bool): Default is for objects to use the "destination" - property of destinations as the target to move to. - Turning off this keyword allows objects to move - "inside" exit objects. - to_none - allow destination to be None. Note that no hooks are run when - moving to a None location. If you want to run hooks, - run them manually (and make sure they can manage None - locations). + Args: + destination (Object): Reference to the object to move to. This + can also be an exit object, in which case the + destination property is used as destination. + quiet (bool): If true, turn off the calling of the emit hooks + (announce_move_to/from etc) + emit_to_obj (Object): object to receive error messages + use_destination (bool): Default is for objects to use the "destination" + property of destinations as the target to move to. Turning off this + keyword allows objects to move "inside" exit objects. + to_none (bool): Allow destination to be None. Note that no hooks are run when + moving to a None location. If you want to run hooks, run them manually + (and make sure they can manage None locations). + move_hooks (bool): If False, turn off the calling of move-related hooks (at_before/after_move etc) + with quiet=True, this is as quiet a move as can be done. - Returns True/False depending on if there were problems with the move. - This method may also return various error messages to the - emit_to_obj. + Returns: + result (bool): True/False depending on if there were problems with the move. + This method may also return various error messages to the + emit_to_obj. """ def logerr(string=""): trc = traceback.format_exc() @@ -609,14 +612,15 @@ class DefaultObject(ObjectDB): destination = destination.destination # Before the move, call eventual pre-commands. - try: - if not self.at_before_move(destination): - return - except Exception: - logerr(errtxt % "at_before_move()") - #emit_to_obj.msg(errtxt % "at_before_move()") - #logger.log_trace() - return False + if move_hooks: + try: + if not self.at_before_move(destination): + return + except Exception: + logerr(errtxt % "at_before_move()") + #emit_to_obj.msg(errtxt % "at_before_move()") + #logger.log_trace() + return False # Save the old location source_location = self.location @@ -630,13 +634,14 @@ class DefaultObject(ObjectDB): source_location = default_home # Call hook on source location - try: - source_location.at_object_leave(self, destination) - except Exception: - logerr(errtxt % "at_object_leave()") - #emit_to_obj.msg(errtxt % "at_object_leave()") - #logger.log_trace() - return False + if move_hooks: + try: + source_location.at_object_leave(self, destination) + except Exception: + logerr(errtxt % "at_object_leave()") + #emit_to_obj.msg(errtxt % "at_object_leave()") + #logger.log_trace() + return False if not quiet: #tell the old room we are leaving @@ -667,25 +672,27 @@ class DefaultObject(ObjectDB): #logger.log_trace() return False - # Perform eventual extra commands on the receiving location - # (the object has already arrived at this point) - try: - destination.at_object_receive(self, source_location) - except Exception: - logerr(errtxt % "at_object_receive()") - #emit_to_obj.msg(errtxt % "at_object_receive()") - #logger.log_trace() - return False + if move_hooks: + # Perform eventual extra commands on the receiving location + # (the object has already arrived at this point) + try: + destination.at_object_receive(self, source_location) + except Exception: + logerr(errtxt % "at_object_receive()") + #emit_to_obj.msg(errtxt % "at_object_receive()") + #logger.log_trace() + return False # Execute eventual extra commands on this object after moving it # (usually calling 'look') - try: - self.at_after_move(source_location) - except Exception: - logerr(errtxt % "at_after_move") - #emit_to_obj.msg(errtxt % "at_after_move()") - #logger.log_trace() - return False + if move_hooks: + try: + self.at_after_move(source_location) + except Exception: + logerr(errtxt % "at_after_move") + #emit_to_obj.msg(errtxt % "at_after_move()") + #logger.log_trace() + return False return True def clear_exits(self): @@ -810,12 +817,6 @@ class DefaultObject(ObjectDB): for script in _ScriptDB.objects.get_all_scripts_on_obj(self): script.stop() - #for script in _GA(self, "scripts").all(): - # script.stop() - - # if self.player: - # self.player.user.is_active = False - # self.player.user.save( # Destroy any exits to and from this room, if any self.clear_exits() @@ -828,7 +829,6 @@ class DefaultObject(ObjectDB): # Perform the deletion of the object super(ObjectDB, self).delete() return True - # methods inherited from the typeclass system def __eq__(self, other): @@ -892,7 +892,6 @@ class DefaultObject(ObjectDB): if cdict.get("location"): cdict["location"].at_object_receive(self, None) self.at_after_move(None) - if cdict.get("attributes"): # this should be a dict of attrname:value keys, values = cdict["attributes"].keys(), cdict["attributes"].values() diff --git a/evennia/typeclasses/models.py b/evennia/typeclasses/models.py index b62020fa92..6b15323c9f 100644 --- a/evennia/typeclasses/models.py +++ b/evennia/typeclasses/models.py @@ -446,7 +446,6 @@ class TypedObject(SharedMemoryModel): self.aliases.clear() if hasattr(self, "nicks"): self.nicks.clear() - self.flush_from_cache() # scrambling properties self.delete = self._deleted diff --git a/evennia/utils/spawner.py b/evennia/utils/spawner.py index b89095c3fc..d25d5080b0 100644 --- a/evennia/utils/spawner.py +++ b/evennia/utils/spawner.py @@ -128,25 +128,27 @@ def _batch_create_object(*objparams): creation/add handlers in the following order: (create, permissions, locks, aliases, nattributes, attributes) Returns: - A list of created objects + objects (list): A list of created objects + """ # bulk create all objects in one go - dbobjs = [ObjectDB(**objparam[0]) for objparam in objparams] - # unfortunately this doesn't work since bulk_create don't creates pks; + + # unfortunately this doesn't work since bulk_create doesn't creates pks; # the result are double objects at the next stage #dbobjs = _ObjectDB.objects.bulk_create(dbobjs) + dbobjs = [ObjectDB(**objparam[0]) for objparam in objparams] objs = [] for iobj, obj in enumerate(dbobjs): # call all setup hooks on each object objparam = objparams[iobj] # setup - obj._createdict = {"pernmissions": objparam[1], + obj._createdict = {"permissions": objparam[1], "locks": objparam[2], "aliases": objparam[3], - "attributes": objparam[4], - "nattributes": objparam[5]} + "nattributes": objparam[4], + "attributes": objparam[5]} # this triggers all hooks obj.save() objs.append(obj)