diff --git a/contrib/tutorial_world/README b/contrib/tutorial_world/README new file mode 100644 index 0000000000..6fddc1d59f --- /dev/null +++ b/contrib/tutorial_world/README @@ -0,0 +1,107 @@ + +=============================================================== + Evennia Tutorial World + + Griatch 2011 +=============================================================== + +This is a stand-alone tutorial area for an unmodified Evennia install. +Think of it as a sort of single-player adventure rather than a +full-fledged multi-player game world. The various rooms and objects +herein are designed to show off features of the engine, not to be a +very challenging (nor long) gaming experience. As such it's of course +only skimming the surface of what is possible. + +================================================================ + Install +================================================================ + +Log in as superuser (#1), then run + + @batchcommand contrib.tutorial_world.build + +Wait for building to complete. This should build the world and +connect it to Limbo. + +Log is as a non-superuser to play the game as intended. The +tutorial area's systems mostly ignores the prescence of a +superuser (so use that to examine things "under the hood" later). + +================================================================ + Comments +================================================================ + +The tutorial world is intended for you playing around with the +engine. It will help you learn how to accomplish some more advanced +effects and might give some good ideas along the way. + +It's suggested you play it through (as a normal user, NOT as +Superuser!) and explore it a bit, then come back here and start +looking into the (heavily documented) source code to find out how +things tick - that's the "tutorial" in Tutorial world after all. + +Please report bugs in the tutorial to the Evennia issue tracker. + + + +* Spoilers below - don't read on unless you already played the +tutorial game. * + + + + + +=============================================================== + Tutorial World Room map +=============================================================== + + ? + | + +---+----+ +-------------------+ +--------+ +--------+ + | | | | |gate | |corner | + | cliff +----+ bridge +----+ +---+ | + | | | | | | | | + +---+---\+ +---------------+---+ +---+----+ +---+----+ + | \ | | castle | + | \ +--------+ +----+---+ +---+----+ +---+----+ + | \ |under- | |ledge | |along | |court- | + | \|ground +--+ | |wall +---+yard | + | \ | | | | | | | + | +------\-+ +--------+ +--------+ +---+----+ + | \ | + ++---------+ \ +--------+ +--------+ +---+----+ + |intro | \ |cell | |trap | |temple | + o--+ | \| +----+ | | | + | | \ | /| | | | + +----+-----+ +--------+ / ---+-+-+-+ +---+----+ + | / | | | | + +----+-----+ +--------+/ +--+-+-+---------+----+ + |outro | |tomb | |antechamber | + o--+ +----------+ | | | + | | | | | | + +----------+ +--------+ +---------------------+ + +Notes: + +o-- connections to/from Limbo +intro/outro areas are rooms that automatically sets/cleans the + Character of any settings incured upon it during the + tutorial game. +The Cliff is a good place to get an overview of the surroundings. +The Bridge may seem like a big room, but it is really only one + room with custom move commands to make it take longer + to cross. You can also fall off the bridge if you + are unlucky or take your time to take in the view too + long. +In the Castle areas an aggressive mob is patrolling. It implements + rudimentary AI but packs quite a punch unless you have + found yourself a weapon that can harm it. Combat is only + possible once you find a weapon. +The Catacombs feature a puzzle for finding the correct Grave + chamber. +The Cell is your reward if you fail in various ways. Finding a + way out of it is a small puzzle of its own. +The Tomb is a nice place to find a weapon that can hurt the + castle guardian. This is infact the goal of the tutorial. + Explore on, or take the exit to finish the tutorial. +? - look into the code if you cannot find this bonus area! diff --git a/contrib/tutorial_world/__init__.py b/contrib/tutorial_world/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/contrib/tutorial_world/build.ev b/contrib/tutorial_world/build.ev new file mode 100644 index 0000000000..491ff4c837 --- /dev/null +++ b/contrib/tutorial_world/build.ev @@ -0,0 +1,991 @@ +# +# Evennia batchfile - tutorial_world +# +# Griatch 2011 +# +# This batchfile sets up a starting tutorial area for Evennia. +# +# This uses the custom script parents and code snippets found in +# the same folder as this script; Note that we are not using any +# modifications of the default player character at all (so you +# don't have to change anything in any settings files). We also +# don't modify any of the default command functions (except in +# states). So bear in mind that the full flexibility of Evennia +# is not used to its fullest here. +# +# If your BATCH_IMPORT_PATH is unchanged from the default, +# first place yourself in the room you want to connect to +# to the tutorial world and load the file as user #1 with +# @batchprocess tutorial/build +# +# The area we are building looks like this: +# +# ? 03,04 +# | +# +---+----+ +-------------------+ +--------+ +--------+ +# | | | | |gate | |corner | +# | cliff +----+ 05 bridge +----+ 09 +---+ 11 | +# | 02 | | | | | | | +# +---+----+ +---------------+---+ +---+----+ +---+----+ +# | \ | | castle | +# | \ +--------+ +----+---+ +---+----+ +---+----+ +# | \ |under- | |ledge | |wall | |court- | +# | \|ground +--+ 06 | | 10 +---+yard | +# | | 07 | | | | | | 12 | +# | +--------+ +--------+ +--------+ +---+----+ +# | \ | +# ++---------+ \ +--------+ +--------+ +---+----+ +# |intro | \ |cell | |trap/ |temple | +# o--+ 01 | \| 08 +----+ fall | | 13 | +# | | | | /| 15 | | | +# +----+-----+ +--------+ / +--+-+-+-+ +---+----+ +# | / | | | | +# +----+-----+ +--------+/ +--+-+-+---------+----+ +# |outro | |tomb | |antechamber | +# o--+ 17 +----------+ 16 | | 14 | +# | | | | | | +# +----------+ +--------+ +---------------------+ +# +# There are a few ways you could build this layout; one is to do all the +# digging in one go first, then go back and add all the details. The +# advantage of this is that you the area is already there and you can +# more easily jump ahead in the build file to the detail work when you +# want to update things later. In this file we will however build and design +# it all in sequence; room by room. This makes it easier to keep an +# overview of what is going on in each room, tie things to parents etc. +# When building your own world you might want to separate your world into +# a lot more individual batch files (maybe one for just a few rooms) for +# easy handling. The numbers mark the order of construction and also +# the unique alias-ids given to each room, to allow safe teleporting +# and linking between them. +# +#------------------------------------------------------------ +# Starting to build the tutorial +# +# This is simple welcome text introducing the tutorial. +#------------------------------------------------------------ +# +# We start from limbo. +@tel #2 +# +# 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. +# +@dig/teleport Intro : tutorial_world.rooms.IntroRoom = tutorial;tut;intro;tut#01 +# +# ... and describe it. +# +@desc +{gWelcome to the Evennia tutorial!{n + + +The following tutorial consists of a small single-player quest area. The various rooms are designed to show off +some of the power and possibilities of the Evennia mud creation system. At any time during this tutorial +you can use the {wtutorial{n (or {wtut{n) command to get some background info about the room or certain objects +to see what is going on "behind the scenes". + + +To get into the mood of this miniature quest, imagine you are an adventurer out to find fame and +fortune. You have heard rumours of an old castle ruin by the coast. In its depth a warrior +princess was buried together with her powerful magical weapon - a valuable prize, if it's true. +Of course this is a chance to adventure that you cannot turn down! + +You reach the coast in the midst of a raging thunderstorm. With wind and rain screaming in your +face you stand where the moor meet the sea along a high, rocky coast ... + + +{g(write 'start' or 'begin' to start the tutorial){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. + +#------------------------------------------------------------ +# +# Outro room +# +# Called from the Intro room; this is a shortcut out. There +# is another outro room at the end showing more text. +# This is the only room we don't give a unique id. +#------------------------------------------------------------ +# +@dig/teleport Leaving Tutorial:tutorial_world.rooms.OutroRoom = exit tutorial;exit;back, start again;start +# +@desc +You are quitting the Evennia tutorial prematurely! Come back later. +# +@open exit = #2 +# +@set here/tutorial_info = +This outro room cleans up properties on the character that was set by the tutorial. +# +# Back to intro room so we can build from there. +# +start +#------------------------------------------------------------ +# +# The cliff +# +#------------------------------------------------------------ +# +# This regularly and randomly shows some weather effects. Note how we +# can spread the command's arguments over more than one line for easy +# reading. we also make sure to create plenty of aliases for the room +# and exits. +# +@dig/teleport Cliff by the sea;cliff;tut#02 + : tutorial_world.rooms.WeatherRoom + = begin adventure;begin;start +# +# We define the tutorial message seen when the using the tutorial command +# +@set here/tutorial_info = +Weather room + +This room inherits from a parent called WeatherRoom. This runs on a +timer that allows various weather-related messages to appear at +irregular intervals. +# +@desc +You stand on the high coast line overlooking a stormy sea far below. Around you the ground is covered +in low grey-green grass, pushed down 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 perched on a sheer cliff out into +the water, isolated from the shore. The only way to cross seems by way of an old hang bridge anchored +not far east from here. +# This is the well you will come back up from if you end up in the underground. +# +@create/drop Old well;well +# +@desc well = +The broken ruins of an old well sit some way off the path. The stones surrounding it have collapsed +and whereas there is still a chain hanging down it, it does not look very secure. It is probably +a remnant of some old settlement back in the day. +# +# It's important to lock the well object or players will be able to pick it up and +# put it in their pocket ... +# +@lock well = get:false() +# +# By setting the lock_msg attribute there will be a nicer error message if people +# try to pick up the well. +# +@set well/get_err_msg = +You probingly nudge the heavy stones of the well. There is no way you can ever +budge this on your own (besides, what would you do with it? Carry it around?). +# +@create/drop Wooden sign;sign : tutorial_world.objects.Readable +# +@desc sign = +The sign sits at the end of a small path leading out to the short-side anchor of the hang +bridge connecting the mainlaind with the ruin on its desolate cliff. The sign is not +as old as the rest of the scenery and the text on it is easily readable. +# +@lock sign = get:false() +# +@set sign/get_err_msg = The sign is securely anchored to the ground. +# +@set sign/readable_text = +WARNING - Bridge is not safe! +# +@set sign/tutorial_info = +This is a readable object, inheriting from a class objects.Readable. The sign has a cmdset with one command +defined on itself called 'read' that 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 hang bridge starting not +far east from you. +# +@lock ruin = get:false() +# +@set ruin/get_err_msg = Small as it appears from a distance, you still cannot reach over and pick up the +castle to put in your pocket. +# +@create/drop The sea (in the distance);sea;ocean +# +@desc sea = +The grey sea stretches as far as the eye can se to the east, and far below you its waves crash +against the foot of the cliff. The vast inland moors 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 grey and barren wasteland. +# +@lock sea = get:false() +# +@set sea/get_err_msg = Noone gets the sea. It gets you. +# +# Set a climbable object for discovering a hidden exit +# +@create/drop gnarled old trees;tree;trees;gnarled : tutorial_world.objects.Climbable +# +@desc trees = Only the sturdiest of trees survive at the edge of the moor. A small group of huddling black things has +dug in near the cliff edge, eternally pummeled by wind and salt to become an integral part of the gloomy scenery. +# +@lock trees = get:false() +# +@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. +# +# The text to echo to player if trying 'climb tree' +# +@set tree/climb_text = +With some effort you climb one of the old trees. + + +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. + + +You climb down again. + +#------------------------------------------------------------ +# Outside Evennia Inn +#------------------------------------------------------------ +@dig Outside Evennia Inn;outside inn;tut#03:tutorial_world.rooms.WeatherRoom + = northern path;north;n;path,back to cliff;back;cliff;south;s +# +# Lock exit from view until we climbed that tree (which is when last_climbed get assigned). +@lock north = view:attr(last_climbed) ; traverse:attr(last_climbed) +# go to outide inn +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 Evennia Inn +# +#------------------------------------------------------------ +@dig/teleport The Evennia Inn;evennia inn;inn;tut#04:tutorial_world.rooms.TutorialRoom = enter;in,leave;out +# +@desc The Evennia Inn consist mainly of one large room filled with tables. The bardisk extends +along the east wall, where multiple barrels and bottles line the shelves. The barkeep seems busy +handing out ale and chatting with the patrons, which are a rowdy and cheerful lot, keeping the sound +level only just below thunderous. + + +Soon you have a beer in hand and is chatting with the locals. Your eye fall on a {wbarrel{n in a corner with a few +old rusty weapons 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 ... +# +@set here/tutorial_info = Nothing special about this room, only a bonus place to go for chatting + with other players. Oh, and don't forget to grab a blade if you don't already have one (only three blades are available +in this location and don'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 ...) +# +@create/drop rusty old sword;rusty;sword;weapon : tutorial_world.objects.Weapon +# +@desc rusty = This is a rusty old broadsword. It has seen better days but the hilt is in good shape. +# +# Only allow to pick up if we don't already has something called weapon +@lock rusty = get:not holds(weapon) +# +@set rusty/get_err_msg = "Now, don't be greedy, friend! You already have a weapon." +# +@create/drop blunt axe;blunt;axe;weapon : tutorial_world.objects.Weapon +# +@desc axe = A heavy weapon, but the edge is dull and covered in rust and nicks. +# +@lock blunt = get:not holds(weapon) +# +@set blunt/get_err_msg = "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 = "You already have a weapon, friend, leave some for the next poor sod." +#------------------------------------------------------------ +# +# The old bridge +# +#------------------------------------------------------------ +# +# 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 itself. +@dig The old bridge;bridge;tut#05 + : tutorial_world.rooms.BridgeRoom + = old bridge;east;e;bridge;hangbridge +# +# put some descriptions on the exit to the bridge +# +@desc bridge = The hang-bridge's foundation sits at the edge of the +cliff to the east - two heavy stone pillars anchors the bridge on this side. +# +# go to the bridge +# +bridge +# +# Set up properties on bridge room (see BridgeRoom) +# +# connect west edge to cliff +# +@set here/west_exit = tut#02 +# +# connect other end to gatehouse +# +@set here/east_exit = tut#09 +# +# Fall location is the cliff ledge +# +@set here/fall_exit = tut#06 +# +@set here/tutorial_info = +Bridge - state room + +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. + +The room also inherits from the weather room to cause the bridge to sway at regular +intervals. It also implements an timer and a random occurance at every step across +the bridge. It might be worth trying this passage a few times to see what may happen. +Hint: you can fall off! + +#------------------------------------------------------------ +# +# Ledge under the bridge +# +#------------------------------------------------------------ +# +# You only end up at the ledge if you fall off the bridge. It +# has no direct connection to the bridge. +# +@dig/teleport Protruding ledge;cliffledge;ledge;tut#06 +# +@set here/tutorial_info = +Ledge + +This room is stored as an attribute on the bridge room and used 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. +# +@desc +You are on a narrow ledge protruding from the side of the cliff, about halfway down. +The air if saturated with salty sea water, sprays hitting your from the crashing +waves below. + +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 cliffside. +# +@create/drop brushes;brush +# +@lock brush = get:false() +# +@desc brush = +The brushes covering the ledge are grey and dwarfed by constantly being pummeled by +salt, rain and wind. +# +@create/drop The sea (far below you);sea;ocean +# +@lock sea = get:false() +# +@desc sea = +Below you rages the grey sea, you can almost imagine that you feel the cliff +tremble 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 hang bridge a fair bit above you, partly obscured +by the rain. 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. + + +#------------------------------------------------------------ +# +# Underground passages +# +#------------------------------------------------------------ +# +# The underground passages allow the player +# to get back up to the cliff again. If you look at the map, +# the cell also connects to here. We'll get to that +# later. +# +@dig Underground passages;passages;underground;tut#07 : tutorial_world.rooms.TutorialRoom = hole into cliff;hole;passage;cliff +# +@desc hole into cliff = +The hole is natural, the soft rock eroded by ages of sea water. The opening is small but +large enough for you to push through. It looks like it expands into a cavern +further in. +# +hole +# +@set here/tutorial_info = +This room acts a hub for getting the player back to the +start again, regardless of how you got here. +# +@desc +The underground cavern system you have entered seems to stretch on forever, +with criss-crossing paths and natural caverns probably carved by water. It is +not completely dark, here and there faint daylight sifts down from above - the +cliff is porous leaving channels of air and light up to the surface. + + +(some time later) + + +You eventually come upon a cavern with a black pool of stale water. In it sits a +murky bucket, the first remnant of any sort of intelligent life down here. The +bucket has disconnected from a chain hanging down from a circular opening high +above. Grey daylight 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 = +The water of the pool is black and opaque. The rain coming down +from above does not seem to ripple the surface quite so much as +it should. +# +@create/drop hole (high above);hole;above +# +@lock hole = get:false() +# +@set hole/get_err_msg = You cannot reach it from here. You need to climb the chain. +# +@desc hole = +Whereas the lower edges of the hole seems jagged and natural you can faintly +make out that it turns into a man-made circular shafts higher up. It looks like +an old well. +# +# From the passages we get back up to the cliff, so we +# open up a new exit back there. +# +# connect chain to Cliff. +@open climb the chain;climb;chain = tut#02 +# +@desc chain = +The chain is made of iron. It is rusty but you think it might still hold +your weight even after all this time. Better hope you don't need to do this more times ... + +#------------------------------------------------------------ +# +# The Dark Cell +# +#------------------------------------------------------------ +# +@dig/teleport Dark cell;dark;cell;tut#08 : tutorial_world.rooms.DarkRoom +# +@set here/tutorial_info = +Dark room + +The dark room implements a custom "dark" state. This is a very +restricted state that completely redefines the look command and only +allows limited interactions. + +Looking around repeatedly will eventually produce +hints as to how to get out of the dark room. +# +# the description is only seen if the player finds a +# light source. +# +@desc +{YThe {yflickering light{Y of the torch reveals a small square cell. It does not seem you are +still in the castle, for the stone of the walls are chiseled crudely and drip with water and mold. + +One wall holds a solid iron-cast door. While rusted and covered with lichen it seems very +sturdy still. In a corner lies what might have once been a bed or a bench but is now +nothing more than a pile or rotting splinters. One of the walls are covered with a thick +cover of black roots where they has broken through the cracks.{n +# +@create/drop iron-cast door;iron;door;iron-cast +# +@lock door = get:false() +# +@desc door = +The door is very solid and clad in iron. No matter how much you push at it, it won't +budge. It seems heavily bolted from the other side. +# +@create/drop stone walls;walls;stone +# +@lock stone = get:false() +# +@desc stone = +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. +# +# The crumbling wall is infact 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 the Underground passages +@set root-covered wall/destination = tut#07 +# +@lock roots = get:false() +# +# (the crumbling wall describes itself, so we don't do it here) + +#------------------------------------------------------------ +# +# Castle Gate +# We are done with the underground, describe castle. +#------------------------------------------------------------ +# +# We are done building the underground passages, let's +# 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. +# +@dig/teleport Ruined gatehouse;gatehouse;tut#09 + : tutorial_world.rooms.TutorialRoom + = , Bridge over the abyss;bridge;abyss;west;w +# +@set here/tutorial_info = +This is part of a four-room area patrolled by a mob; the guardian +of the castle. The mob initiates combat if the player stays in the same room +for long enough. + +Combat itself is a very simple affair which takes advantage of the strength +of the weapon you use, but dictates a fix skill for you and your enemy. The enemy +is quite powerful, so don't stick around too long ... +# +@desc +The old gatehouse is near collapse. Part of its northern +wall has already fallen down, together with parts of the fortifications in that direction. +Heavy stone pillars hold up sections of ceiling, but elsewhere the flagstones are exposed +to open sky. Part of a heavy portuculis, formerly blocking off the inner castle from attack, +is sprawled over the ground together with most of its frame. + +{wEast{n the gatehouse leads out to a small open area surrounded by the remains of the castle. +There is also an archway standing 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 = +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 +# +@lock bridge = traverse:not attr(is_mob) + +#------------------------------------------------------------ +# +# Along the southern inner wall (south from gatehouse) +# +#------------------------------------------------------------ + +@dig Along inner wall;inner wall;along;tut#10 : tutorial_world.rooms.WeatherRoom = + Standing archway;archway;south;s,ruined gatehouse;gatehouse;north;n +# +@desc standing archway = +It seems the archway leads off into a series of dimly lit rooms. +# +archway +# +@set here/tutorial_info = +This is part of a four-room area patrolled by a mob; the guardian +of the castle. The mob initiates combat if the player stays in the same room +for long enough. + +Combat itself is a very simple affair which takes advantage of the strength +of the weapon you use, but dictates a fix skill for you and your enemy. +# +@desc +What appears at first sight to be a series of connected rooms actually +turns out to be collapsed buildings so mashed together by the ravashes of time +that they all seem to lean on each other and against the outer wall. The whole scene +is directly open to the sky. + +The buildings make a half-circle along the main wall, here and there broken by falling +stone and rubble. At one end (the {wnorth{nern) of this half-circle is the entrance to the castle, the ruined +gatehoue. {wEast{nwards from here is some sort of open courtyard. + +#------------------------------------------------------------ +# +# Corner of castle (east from gatehouse) +# +#------------------------------------------------------------ +# back to castle gate +@teleport tut#09 +# +@dig/teleport Corner of castle ruins;corner;tut#11:tutorial_world.rooms.TutorialRoom = castle corner;corner;east;e,gatehouse;west;w +# +@desc The ruins opens up to the sky in a small open area, lined by collumns. The open area is +dominated by a huge stone obelisk in its center, an ancient ornament miraculously still standing. + +Previously one could probably continue past the obelisk and eastward into the castle keep itself, +but that way is now completely blocked by falled rubble. To the {wwest{n is the gatehouse and +entrance to the castle, whereas {wsouth{nwards the collumns make way for a wide open courtyard. +# +@set here/tutorial_info = +This is part of a four-room area patrolled by a mob; the guardian +of the castle. The mob initiates combat if the player stays in the same room +for long enough. + +Combat itself is a very simple affair which takes advantage of the strength +of the weapon you use, but dictates a fix skill for you and your enemy. +# +@create/drop obelisk:tutorial_world.objects.Obelisk +# +@lock obelisk = get:false() +# +@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) +# +# Create the mobile. This is the start location. +@create/drop Ghostly apparition;ghost;apparition;fog : tutorial_world.mob.Enemy +# +@set ghost/full_health = 20 +# +@set ghost/defeat_location = dark cell +# +@lock ghost = get:false() +# +@set ghost/get_err_msg = Your fingers just pass straight through it! +# +@desc ghost = +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 seems to form limbs and even faces that fade +away only moments later. The thing reeks of almost tangible spite at your presence. This must be the ruin's eternal guardian. +# +# Give the enemy some random echoes (echoed at irregular intervals) +@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.] +# +# give the enemy a weapon +# +@create foggy tentacles;tentacles:tutorial_world.objects.Weapon +# +# Make the enemy good +# +@set foggy tentacles/hit = 0.7 +# +@teleport/quiet tentacles = ghost +# +# Clear inactive mode and start the mob +# +@set ghost/inactive = + +#------------------------------------------------------------ +# +# The courtyard +# +#------------------------------------------------------------ +# +@dig/teleport Overgrown courtyard;courtyard;tut#12 : tutorial_world.rooms.WeatherRoom = courtyard;south;s,castle corner;north;n +# +# Connect west to the inner wall +@open along inner wall;wall;along;west;w, overgrown courtyard;courtyard;east;e = tut#10 +# +@set here/tutorial_info = +This is part of a four-room area patrolled by a mob; the guardian +of the castle. The mob initiates combat if the player stays in the same room +for long enough. + +Combat itself is a very simple affair which takes advantage of the strength +of the weapon you use, but dictates a fix skill for you and your enemy. +# +@desc The inner courtyard of the old castle is littered with debris and overgrown +with low grass and patches of thorny wines. There is a collapsed structure close to +the gatehouse that looks like a stable. + +{wNorth{nwards is a smaller area cornered in the debris, adorned with a +looming obelisk-like thing. To the {wwest{n the castle walls loom over a +mess of collapsed buildings. On the opposite, {weast{nern side of the yard is a +large building with a curved roof that seem to have 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 = +The building is empty, if it was indeed once a stable it was abandoned long ago. + +#------------------------------------------------------------ +# +# The temple +# +#------------------------------------------------------------ +# +@dig/teleport The ruined temple;temple;in;tut#13:tutorial_world.rooms.TutorialRoom = ruined temple;temple;east;e, overgrown courtyard;courtyard;outside;out;west;w +# + +# +@desc +This building seems to have survived the ravages of time better than most +of the others. Its arched roof and wide spaces suggests that this is a temple +or church of some kind. + + +The wide hall of the temple stretches before you. At the far edge is a +stone altar with no clear markings. Despite its relatively good condition, +the temple is empty of all furniture or valuables, like it was looted or its +treasures moved ages ago. + +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. + +#------------------------------------------------------------ +# +# Antechamber +# +#------------------------------------------------------------ +# +@dig Antechamber;antechamber;tut#14 + : tutorial_world.rooms.TutorialRoom + = + stairs down;stairs;down;d, + up the stairs to ruined temple;stairs;temple;up;u +# +@desc stairs down = +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 is_mob +# +stairs down +# +@desc +This chamber lies almost directly under the main altar of the temple. The +passage of aeons is felt here and you also sense you are close to great power. + +The sides of the chamber are lined with stone archways, these are entrances to +the {wtombs{n of what must have been influental families or individual heroes of +the realm. Each is adorned by a stone statue or symbol of fine make. They do not seem to be +ordered in any particular order or rank. +# +@set here/tutorial_info = +This is the second part of a puzzle involving the Obelisk in another room. The +correct exit will vary depending on which scene was shown on the Obelisk surface. + +Each tomb is a teleporter room and is keyed to a number corresponding to the scene +last shown on the obelisk (now stored on player). If the number doesn't match, the tomb +teleports to the Dark Cell. If correct, the tomb teleports to the Ancient Tomb treasure +chamber. +# +# We don't put unique ids on the individual tombs +# +@dig 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 +# +@tel Blue bird tomb +# +@set here/puzzle_value = 0 +# +@set here/failure_teleport_to = falling! +# +@set here/success_teleport_to = Ancient tomb +# +@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. +#------------------------------------------------------------ +@dig/teleport Falling!;falling;tut#15: tutorial_world.rooms.TeleportRoom +# +@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. + +{rSuddenly you hear a distinct 'click' and the ground suddenly 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 = dark cell +# +@set here/failure_teleport_to = dark cell +# +# back to antechamber +@tel tut#14 +#------------------------------------------------------------ +# +# The ancient tomb +# +#------------------------------------------------------------ +# Create the real tomb +# +@dig/teleport Ancient tomb;tut#16 + : tutorial_world.rooms.TutorialRoom = ,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 marked symbols of hawks and +the face of a stern woman, clearly some sort of ancient hero. +# +@set here/tutorial_info = +Congratulations, you have reached the end of this little mini-quest. Just +grab the mythical weapon (get weapon) and the exit will open. + +You can end the quest here or go back through the tutorial rooms to +explore further. +# +@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 ... +# +@set sarcophagus/rack_id = rack_sarcophagus +# +@set sarcophagus/min_dmg = 4.0 +# +@set sarcophagus/max_dmg = 11.0 +# +@set sarcophagus/magic = True +# +@set sarcophagus/get_text = +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! + +With trembling hands you release the weapon from the stone and hold {c%s{n in your hands! + + + + +{gThis concludes this tutorial. From here you can either continue to explore the castle (hint: this weapon +works better against the castle guardian than any you might have found earlier) or you can exit.{n + +#------------------------------------------------------------ +# +# Outro - end of the tutorial +# +#------------------------------------------------------------ +# +@dig End of tutorial;end;tut#17 : tutorial_world.rooms.OutroRoom = Exit tutorial;exit;end +# +# All weapons from the rack gets an automatic alias the same as the rack_id. This we can +# use to check if any such weapon is in inventory before unlocking the exit. +# +@lock Exit tutorial: view:holds(rack_sarcophagus) ; traverse:holds(rack_sarcophagus) +# +# to tutorial outro +@tel tut#17 +# +# this quits the tutorial and cleans up all variables that was . +@desc +{gThanks for trying out this little Evennia tutorial! + + +The examples given here are of course just scraping the surface of what +can be done. The tutorial focuses more on showing various techniques than any sort of +novel storytelling or challenging gameplay. The full README and source code for the +tutorial world can be found in {wcontrib/tutorial_world{g. + + +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 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 +# +@set here/tutorial_info = +This room cleans up all temporary attributes that was put on the character during the tutorial. +# +# Tie this back to Limbo +# +@open exit back to Limbo;limbo;exit;back = #2 +# +@tel 2 \ No newline at end of file diff --git a/contrib/tutorial_world/mob.py b/contrib/tutorial_world/mob.py new file mode 100644 index 0000000000..e23fba88d8 --- /dev/null +++ b/contrib/tutorial_world/mob.py @@ -0,0 +1,347 @@ +""" +This module implements a simple mobile object with +a very rudimentary AI as well as an aggressive enemy +object based on that mobile class. + +""" + +import random, time +from django.conf import settings + +from src.objects.models import ObjectDB +from src.utils import utils +from game.gamesrc.scripts.basescript import Script +from contrib.tutorial_world import objects as tut_objects +from contrib.tutorial_world import scripts as tut_scripts + +BASE_CHARACTER_TYPECLASS = settings.BASE_CHARACTER_TYPECLASS + +#------------------------------------------------------------ +# +# Mob - mobile object +# +# This object utilizes exits and moves about randomly from +# room to room. +# +#------------------------------------------------------------ + +class Mob(tut_objects.TutorialObject): + """ + This type of mobile will roam from exit to exit at + random intervals. Simply lock exits against the is_mob attribute + to block them from the mob (lockstring = "traverse:not attr(is_mob)"). + """ + def at_object_creation(self): + "This is called when the object is first created." + self.db.tutorial_info = "This is a moving object. It moves randomly from room to room." + + self.scripts.add(tut_scripts.IrregularEvent) + # this is a good attribute for exits to look for, to block + # a mob from entering certain exits. + self.db.is_mob = True + self.db.last_location = None + # only when True will the mob move. + self.db.roam_mode = True + + def announce_move_from(self, destination): + "Called just before moving" + self.location.msg_contents("With a cold breeze, %s drifts in the direction of %s." % (self.key, destination.key)) + + def announce_move_to_(self, source_location): + "Called just after arriving" + self.location.msg_contents("With a wailing sound, %s appears from the %s." % (self.key, source_location.key)) + + def update_irregular(self): + "Called at irregular intervals. Moves the mob." + if self.roam_mode: + exits = [ex for ex in self.location.exits if self.access(ex, "traverse")] + if exits: + # Try to make it so the mob doesn't backtrack. + new_exits = [ex for ex in exits if ex.destination != self.db.last_location] + if new_exits: + exits = new_exits + self.db.last_location = self.location + self.execute_cmd("%s" % exits[random.randint(0, len(exits) - 1)].key) + + +#------------------------------------------------------------ +# +# Enemy - mobile attacking object +# +# An enemy is a mobile that is aggressive against players +# in its vicinity. An enemy will try to attack characters +# in the same location. It will also pursue enemies through +# exits if possible. +# +# An enemy needs to have a Weapon object in order to +# attack. +# +# This particular tutorial enemy is a ghostly apparition that can only +# be hurt by magical weapons. It will also not truly "die", but only +# teleport to another room. Players defeated by the apparition will +# conversely just be teleported to a holding room. +# +#------------------------------------------------------------ + +class AttackTimer(Script): + """ + This script is what makes an eneny "tick". + """ + def at_script_creation(self): + "This sets up the script" + self.key = "AttackTimer" + self.desc = "Drives an Enemy's combat." + self.interval = random.randint(10, 15) # how fast the Enemy acts + self.start_delay = True # wait self.interval before first call + self.persistent = True + def at_repeat(self): + "Called every self.interval seconds." + if self.obj.db.inactive: + return + if self.obj.db.roam_mode: + self.obj.roam() + elif self.obj.db.battle_mode: + self.obj.attack() + elif self.obj.db.pursue_mode: + self.obj.pursue() + else: + #dead mode. Wait for respawn. + dead_at = self.db.dead_at + if not dead_at: + self.db.dead_at = time.time() + if (time.time() - self.db.dead_at) > self.db.dead_timer: + self.obj.reset() + +class Enemy(Mob): + """ + This is a ghostly enemy with health (hit points). Their chance to hit, damage etc is + determined by the weapon they are wielding, same as characters. + + An enemy can be in four modes: + roam (inherited from Mob) - where it just moves around randomly + battle - where it stands in one place and attacks players + pursue - where it follows a player, trying to enter combat again + dead - passive and invisible until it is respawned + + Upon creation, the following attributes describe the enemy's actions + desc - description + full_health - integer number > 0 + defeat_location - unique name or #dbref to the location the player is taken when defeated. If not given, will remain in room. + defeat_text - text to show player when they are defeated (just before being whisped away to defeat_location) + defeat_text_room - text to show other players in room when a player is defeated + win_text - text to show player when defeating the enemy + win_text_room - text to show room when a player defeates the enemy + respawn_text - text to echo to room when the mob is reset/respawn in that room. + + """ + def at_object_creation(self): + "Called at object creation." + super(Enemy, self).at_object_creation() + + self.db.tutorial_info = "This moving object will attack players in the same room." + + # state machine modes + self.db.roam_mode = True + self.db.battle_mode = False + self.db.pursue_mode = False + self.db.dead_mode = False + # health (change this at creation time) + self.db.full_health = 20 + self.db.health = 20 + self.db.dead_at = time.time() + self.db.dead_timer = 100 # how long to stay dead + self.db.inactive = True # this is used during creation to make sure the mob doesn't move away + # store the last player to hit + self.db.last_attacker = None + # where to take defeated enemies + self.db.defeat_location = "darkcell" + self.scripts.add(AttackTimer) + + def update_irregular(self): + "the irregular event is inherited from Mob class" + strings = self.db.irregular_echoes + if strings: + self.location.msg_contents(strings[random.randint(0, len(strings) - 1)]) + + def roam(self): + "Called by Attack timer. Will move randomly as long as exits are open." + + # in this mode, the mob is healed. + self.db.health = self.db.full_health + players = [obj for obj in self.location.contents + if utils.inherits_from(obj, BASE_CHARACTER_TYPECLASS) and not obj.is_superuser] + if players: + # we found players in the room. Attack. + self.roam_mode = False + self.db.battle_mode = True + self.attack() + elif random.random() < 0.2: + # no players to attack, move about randomly. + exits = [ex.destination for ex in self.location.exits if ex.access(self, "traverse")] + if exits: + # Try to make it so the mob doesn't backtrack. + new_exits = [ex for ex in exits if ex.destination != self.db.last_location] + if new_exits: + exits = new_exits + self.db.last_location = self.location + self.move_to(exits[random.randint(0, len(exits) - 1)]) + else: + # no exits - a dead end room. Respawn back to start. + self.move_to(self.home) + + def attack(self): + """ + This is the main mode of combat. It will try to hit players in + the location. If players are defeated, it will whisp them off + to the defeat location. + """ + last_attacker = self.db.last_attacker + players = [obj for obj in self.location.contents + if utils.inherits_from(obj, BASE_CHARACTER_TYPECLASS) and not obj.is_superuser] + if players: + + # find a target + if last_attacker in players: + # prefer to attack the player last attacking. + target = last_attacker + else: + # otherwise attack a random player in location + target = players[random.randint(0, len(players) - 1)] + + # try to use the weapon in hand + attack_cmds = ("thrust", "pierce", "stab", "slash", "chop") + cmd = attack_cmds[random.randint(0, len(attack_cmds) - 1)] + self.execute_cmd("%s %s" % (cmd, target)) + + # analyze result. + if target.db.health <= 0: + # we reduced enemy to 0 health. Whisp them off to the prison room. + tloc = ObjectDB.objects.object_search(self.db.defeat_location, global_search=True) + tstring = self.db.defeat_text + if not tstring: + tstring = "You feel your conciousness slip away ... you fall to the ground as " + tstring += "the misty apparition envelopes you ...\n The world goes black ...\n" + target.msg(tstring) + ostring = self.db.defeat_text_room + if tloc: + if not ostring: + ostring = "\n%s envelops the fallen ... and then their body is suddenly gone!" % self.key + # silently move the player to defeat location (we need to call hook manually) + target.location = tloc[0] + tloc[0].at_object_receive(target, self.location) + elif not ostring: + ostring = "%s falls to the ground!" % target.key + self.location.msg_contents(ostring, exclude=[target]) + else: + # no players found, this could mean they have fled. Switch to pursue mode. + self.battle_mode = False + self.roam_mode = False + self.pursue_mode = True + + def pursue(self): + """ + In pursue mode, the enemy tries to find players in adjoining rooms, preferably + those that previously attacked it. + """ + last_attacker = self.db.last_attacker + players = [obj for obj in self.location.contents if utils.inherits_from(obj, BASE_CHARACTER_TYPECLASS)] + if players: + # we found players in the room. Maybe we caught up with some, or some walked in on us + # before we had time to pursue them. Switch to battle mode. + self.battle_mode = True + self.roam_mode = False + self.pursue_mode = False + self.attack() + else: + # find all possible destinations. + destinations = [ex.destination for ex in self.location.exits if ex.access(self, "traverse")] + # find all players in the possible destinations. OBS-we cannot just use the player's + # current position to move the Enemy; this might have changed when the move is performed, + # causing the enemy to teleport out of bounds. + players = {} + for dest in destinations: + for obj in [o for o in dest.contents if utils.inherits_from(o, BASE_CHARACTER_TYPECLASS)]: + players[obj] = dest + if players: + # we found targets. Move to intercept. + if last_attacker in players: + # preferably the one that last attacked us + self.move_to(players[last_attacker]) + else: + # otherwise randomly. + key = players.keys()[random.randint(0, len(players) - 1)] + self.move_to(players[key]) + else: + # we found no players nearby. Return to roam mode. + self.battle_mode = False + self.roam_mode = True + self.pursue_mode = False + + def at_hit(self, weapon, attacker, damage): + """ + Called when this object is hit by an enemy's weapon + Should return True if enemy is defeated, False otherwise. + + In the case of players attacking, we handle all the events + and information from here, so the return value is not used. + """ + + self.db.last_attacker = attacker + if not self.battle_mode: + # we were attacked, so switch to battle mode. + self.db.roam_mode = False + self.db.pursue_mode = False + self.db.battle_mode = True + #self.scripts.add(AttackTimer) + + if not weapon.db.magic: + # In the tutorial, the enemy is a ghostly apparition, so + # only magical weapons can harm it. + string = self.db.weapon_ineffective_text + if not string: + string = "Your weapon just passes through your enemy, causing no effect!" + attacker.msg(string) + return + else: + # an actual hit + health = float(self.db.health) + health -= damage + self.db.health = health + if health <= 0: + string = self.db.win_text + if not string: + string = "After your last hit, %s folds in on itself, it seems to fade away into nothingness. " % self.key + string += "In a moment there is nothing left but the echoes of its screams. But you have a " + string += "feeling it is only temporarily weakened. " + string += "You fear it's only a matter of time before it materializes somewhere again." + attacker.msg(string) + string = self.db.win_text_room + if not string: + string = "After %s's last hit, %s folds in on itself, it seems to fade away into nothingness. " % (attacker.name, self.key) + string += "In a moment there is nothing left but the echoes of its screams. But you have a " + string += "feeling it is only temporarily weakened. " + string += "You fear it's only a matter of time before it materializes somewhere again." + self.location.msg_contents(string, exclude=[attacker]) + + # put enemy in dead mode and hide it from view. IrregularEvent(or a world reset) will bring it back later. + self.db.roam_mode = False + self.db.pursue_mode = False + self.db.battle_mode = False + self.db.dead_mode = True + self.db.dead_at = time.time() + self.location = None + return False + + def reset(self): + "If the mob was 'dead', respawn it to its home position and reset all modes and damage." + if self.db.dead_mode: + self.db.health = self.db.full_health + self.db.roam_mode = True + self.db.pursue_mode = False + self.db.battle_mode = False + self.db.dead_mode = False + self.location = self.home + string = self.db.respawn_text + if not string: + string = "%s fades into existence from out of thin air. It's looking pissed." % self.key + self.location.msg_contents(string) diff --git a/contrib/tutorial_world/objects.py b/contrib/tutorial_world/objects.py new file mode 100644 index 0000000000..c2974283db --- /dev/null +++ b/contrib/tutorial_world/objects.py @@ -0,0 +1,902 @@ +""" +TutorialWorld - basic objects - Griatch 2011 + +This module holds all "dead" object definitions for +the tutorial world. Object-commands and -cmdsets +are also defined here, together with the object. + +Objects: + +TutorialObject + +Readable +Obelisk +LightSource +CrumblingWall +Weapon + +""" + +import time, random + +from src.utils import utils, create +from game.gamesrc.objects.baseobjects import Object, Exit +from game.gamesrc.commands.basecommand import Command +from game.gamesrc.commands.basecmdset import CmdSet +from game.gamesrc.scripts.basescript import Script + +#------------------------------------------------------------ +# +# TutorialObject +# +# The TutorialObject is the base class for all items +# in the tutorial. They have an attribute "tutorial_info" +# on them that a global tutorial command can use to extract +# interesting behind-the scenes information about the object. +# +# TutorialObjects may also be "reset". What the reset means +# is up to the object. It can be the resetting of the world +# itself, or the removal of an inventory item from a +# character's inventory when leaving the tutorial, for example. +# +#------------------------------------------------------------ + + +class TutorialObject(Object): + """ + This is the baseclass for all objects in the tutorial. + """ + + def at_object_creation(self): + "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." + self.location = self.home + + +#------------------------------------------------------------ +# +# Readable - an object one can "read". +# +#------------------------------------------------------------ + +class CmdRead(Command): + """ + Usage: + read [obj] + + Read some text. + """ + + key = "read" + locks = "cmd:all()" + help_category = "TutorialWorld" + + def func(self): + "Implement the read command." + if self.args: + obj = self.caller.search(self.args.strip()) + else: + obj = self.obj + if not obj: + return + # we want an attribute read_text to be defined. + readtext = obj.db.readable_text + if readtext: + string = "You read {C%s{n:\n %s" % (obj.key, readtext) + else: + string = "There is nothing to read on %s." % obj.key + self.caller.msg(string) + +class CmdSetReadable(CmdSet): + "CmdSet for readables" + def at_cmdset_creation(self): + "called when object is created." + self.add(CmdRead()) + +class Readable(TutorialObject): + """ + This object defines some attributes and defines a read method on itself. + """ + def at_object_creation(self): + "Called when object is created" + super(Readable, self).at_object_creation() + self.db.tutorial_info = "This is an object with a 'read' command defined in a command set on itself." + self.db.readable_text = "There is no text written on %s." % self.key + # define a command on the object. + self.cmdset.add_default(CmdSetReadable, permanent=True) + + +#------------------------------------------------------------ +# +# Climbable object +# +# The climbable object works so that once climbed, it sets +# a flag on the climber to show that it was climbed. A simple +# command 'climb' handles the actual climbing. +# +#------------------------------------------------------------ + +class CmdClimb(Command): + """ + Usage: + climb + """ + key = "climb" + locks = "cmd:all()" + help_category = "TutorialWorld" + + def func(self): + "Implements function" + + if not self.args: + self.caller.msg("What do you want to climb?") + return + obj = self.caller.search(self.args.strip()) + if not obj: + return + if obj != self.obj: + self.caller.msg("Try as you might, you cannot climb that.") + return + ostring = self.obj.db.climb_text + if not ostring: + ostring = "You climb %s. Having looked around, you climb down again." % self.obj.name + self.caller.msg(ostring) + self.caller.db.last_climbed = self.obj + +class CmdSetClimbable(CmdSet): + "Climbing cmdset" + def at_cmdset_creation(self): + "populate set" + self.add(CmdClimb()) + + +class Climbable(TutorialObject): + "A climbable object." + + def at_object_creation(self): + "Called at initial creation only" + self.cmdset.add_default(CmdSetClimbable, permanent=True) + + + +#------------------------------------------------------------ +# +# Obelisk - a unique item +# +# The Obelisk is an object with a modified return_appearance +# method that causes it to look slightly different every +# time one looks at it. Since what you actually see +# is a part of a game puzzle, the act of looking also +# stores a key attribute on the looking object for later +# reference. +# +#------------------------------------------------------------ + +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. + """ + + 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." + # make sure this can never be picked up + self.locks.add("get:false()") + + def return_appearance(self, caller): + "Overload the default version of this hook." + clueindex = random.randint(0, len(OBELISK_DESCS)-1) + # set this description + string = "The surface of the obelisk seem to waver, shift and writhe under your gaze, with " + string += "different scenes and structures appearing whenever you look at it. " + self.db.desc = string + OBELISK_DESCS[clueindex] + # remember that this was the clue we got. + caller.db.puzzle_clue = clueindex + # call the parent function as normal (this will use db.desc we just set) + return super(Obelisk, self).return_appearance(caller) + +#------------------------------------------------------------ +# +# LightSource +# +# This object that emits light and can be +# turned on or off. It must be carried to use and has only +# a limited burn-time. +# When burned out, it will remove itself from the carrying +# character's inventory. +# +#------------------------------------------------------------ + +class StateLightSourceOn(Script): + """ + This script controls how long the light source is burning. When + it runs out of fuel, the lightsource goes out. + """ + def at_script_creation(self): + "Called at creation of script." + self.key = "lightsourceBurn" + self.desc = "Keeps lightsources burning." + self.start_delay = True # only fire after self.interval s. + self.repeats = 1 # only run once. + self.persistent = True # survive a server reboot. + def at_start(self): + "Called at script start - this can also happen if server is restarted." + self.interval = self.obj.db.burntime + self.db.script_started = time.time() + def at_stop(self): + """ + Since this script stops after only 1 "repeat", we can use this hook + instead of at_repeat(). Since the user may also turn off the light + prematurely, this hook will also be called in that case. + """ + # calculate remaining burntime + time_burnt = time.time() - self.db.script_started + burntime = self.interval - time_burnt + self.obj.db.burntime = burntime + if burntime <= 0: + # no burntime left. Reset the object. + self.obj.reset() + def is_valid(self): + "This script is only valid as long as the lightsource burns." + return self.obj.db.is_active + +class CmdLightSourceOn(Command): + """ + Switches on the lightsource. + """ + key = "on" + aliases = ["switch on", "turn on", "light"] + locks = "cmd:holds()" # only allow if command.obj is carried by caller. + help_category = "TutorialWorld" + + def func(self): + "Implements the command" + + if self.obj.db.is_active: + self.caller.msg("%s is already burning." % self.obj.key) + else: + # set lightsource to active + self.obj.db.is_active = True + # activate the script to track burn-time. + self.obj.scripts.add(StateLightSourceOn) + self.caller.msg("{gYou light {C%s.{n" % self.obj.key) + self.caller.location.msg_contents("%s lights %s!" % (self.caller, self.obj.key), exclude=[self.caller]) + # we run script validation on the room to make light/dark states tick. + self.caller.location.scripts.validate() + # look around + self.caller.execute_cmd("look") + + +class CmdLightSourceOff(Command): + """ + Switch off the lightsource. + """ + key = "off" + aliases = ["switch off", "turn off", "dowse"] + locks = "cmd:holds()" # only allow if command.obj is carried by caller. + help_category = "TutorialWorld" + + def func(self): + "Implements the command " + + if not self.obj.db.is_active: + self.caller.msg("%s is not burning." % self.obj.key) + else: + # set lightsource to inactive + self.obj.db.is_active = False + # validating the scripts will kill it now that is_active=False. + self.obj.scripts.validate() + self.caller.msg("{GYou dowse {C%s.{n" % self.obj.key) + self.caller.location.msg_contents("%s dowses %s." % (self.caller, self.obj.key), exclude=[self.caller]) + self.caller.location.scripts.validate() + self.caller.execute_cmd("look") + # we run script validation on the room to make light/dark states tick. + + +class CmdSetLightSource(CmdSet): + "CmdSet for the lightsource commands" + key = "lightsource_cmdset" + def at_cmdset_creation(self): + "called at cmdset creation" + self.add(CmdLightSourceOn()) + self.add(CmdLightSourceOff()) + +class LightSource(TutorialObject): + """ + This implements a light source object. + + When burned out, lightsource will be moved to its home - which by default is the + location it was first created at. + """ + def at_object_creation(self): + "Called when object is first created." + super(LightSource, self).at_object_creation() + self.db.tutorial_info = "This object can be turned on off and has a timed script controlling it." + self.db.is_active = False + self.db.burntime = 60 # 1 minute + self.db.desc = "A splinter of wood with remnants of resin on it, enough for burning." + # add commands + self.cmdset.add_default(CmdSetLightSource, permanent=True) + + def reset(self): + """ + Can be called by tutorial world runner, or by the script when the lightsource + has burned out. + """ + if self.db.burntime <= 0: + # light burned out. Since the lightsources's "location" should be + # a character, notify them this way. + try: + loc = self.location.location + except AttributeError: + loc = self.location + loc.msg_contents("{c%s{n {Rburns out.{n" % self.key) + self.db.is_active = False + try: + # validate in holders current room, if possible + self.location.location.scripts.validate() + except AttributeError: + # maybe it was dropped, try validating at current location. + try: + self.location.scripts.validate() + except AttributeError,e: + pass + self.delete() + +#------------------------------------------------------------ +# +# Crumbling wall - unique exit +# +# This implements a simple puzzle exit that needs to be +# accessed with commands before one can get to traverse it. +# +# The puzzle is currently simply to move roots (that have +# presumably covered the wall) aside until a button for a +# secret door is revealed. The original position of the +# roots blocks the button, so they have to be moved to a certain +# position - when they have, the "press button" command +# is made available and the Exit is made traversable. +# +#------------------------------------------------------------ + +# There are four roots - two horizontal and two vertically +# running roots. Each can have three positions: top/middle/bottom +# and left/middle/right respectively. There can be any number of +# roots hanging through the middle position, but only one each +# along the sides. The goal is to make the center position clear. +# (yes, it's really as simple as it sounds, just move the roots +# to each side to "win". This is just a tutorial, remember?) + +class CmdShiftRoot(Command): + """ + Shifts roots around. + + shift blue root left/right + shift red root left/right + shift yellow root up/down + shift green root up/down + + """ + key = "shift" + aliases = ["move"] + # the locattr() lock looks for the attribute is_dark on the current room. + locks = "cmd:not locattr(is_dark)" + help_category = "TutorialWorld" + + def parse(self): + "custom parser; split input by spaces" + self.arglist = self.args.strip().split() + + def func(self): + """ + Implement the command. + blue/red - vertical roots + yellow/green - horizontal roots + """ + + if not self.arglist: + self.caller.msg("What do you want to move, and in what direction?") + return + if "root" in self.arglist: + self.arglist.remove("root") + # we accept arguments on the form + if not len(self.arglist) > 1: + self.caller.msg("You must define which colour of root you want to move, and in which direction.") + return + color = self.arglist[0].lower() + direction = self.arglist[1].lower() + # get current root positions dict + root_pos = self.obj.db.root_pos + + if not color in root_pos: + self.caller.msg("No such root to move.") + return + + # first, vertical roots (red/blue) - can be moved left/right + if color == "red": + if direction == "left": + root_pos[color] = max(-1, root_pos[color] - 1) + self.caller.msg("You shift the reddish root to the left.") + if root_pos[color] != 0 and root_pos[color] == root_pos["blue"]: + root_pos["blue"] += 1 + self.caller.msg("The root with blue flowers gets in the way and is pushed to the right.") + elif direction == "right": + root_pos[color] = min(1, root_pos[color] + 1) + self.caller.msg("You shove the reddish root to the right.") + if root_pos[color] != 0 and root_pos[color] == root_pos["blue"]: + root_pos["blue"] -= 1 + self.caller.msg("The root with blue flowers gets in the way and is pushed to the left.") + else: + self.caller.msg("You cannot move the root in that direction.") + elif color == "blue": + if direction == "left": + root_pos[color] = max(-1, root_pos[color] - 1) + self.caller.msg("You shift the root with small blue flowers to the left.") + if root_pos[color] != 0 and root_pos[color] == root_pos["red"]: + root_pos["red"] += 1 + self.caller.msg("The reddish root is to big to fit as well, so that one falls away to the left.") + elif direction == "right": + root_pos[color] = min(1, root_pos[color] + 1) + self.caller.msg("You shove the root adorned with small blue flowers to the right.") + if root_pos[color] != 0 and root_pos[color] == root_pos["red"]: + root_pos["red"] -= 1 + self.caller.msg("The thick reddish root gets in the way and is pushed back to the left.") + else: + self.caller.msg("You cannot move the root in that direction.") + # now the horizontal roots (yellow/green). They can be moved up/down + elif color == "yellow": + if direction == "up": + root_pos[color] = max(-1, root_pos[color] - 1) + self.caller.msg("You shift the root with small yellow flowers upwards.") + if root_pos[color] != 0 and root_pos[color] == root_pos["green"]: + root_pos["green"] += 1 + self.caller.msg("The green weedy root falls down.") + elif direction == "down": + root_pos[color] = min(1, root_pos[color] +1) + self.caller.msg("You shove the root adorned with small yellow flowers downwards.") + if root_pos[color] != 0 and root_pos[color] == root_pos["green"]: + root_pos["green"] -= 1 + self.caller.msg("The weedy green root is shifted upwards to make room.") + else: + self.caller.msg("You cannot move the root in that direction.") + elif color == "green": + if direction == "up": + root_pos[color] = max(-1, root_pos[color] - 1) + self.caller.msg("You shift the weedy green root upwards.") + if root_pos[color] != 0 and root_pos[color] == root_pos["yellow"]: + root_pos["yellow"] += 1 + self.caller.msg("The root with yellow flowers falls down.") + elif direction == "down": + root_pos[color] = min(1, root_pos[color] + 1) + self.caller.msg("You shove the weedy green root downwards.") + if root_pos[color] != 0 and root_pos[color] == root_pos["yellow"]: + root_pos["yellow"] -= 1 + self.caller.msg("The root with yellow flowers gets in the way and is pushed upwards.") + else: + self.caller.msg("You cannot move the root in that direction.") + # store new position + self.obj.db.root_pos = root_pos + # check victory condition + if root_pos.values().count(0) == 0: # no roots in middle position + self.caller.db.crumbling_wall_found_button = True + self.caller.msg("Holding aside the root you think you notice something behind it ...") + +class CmdPressButton(Command): + """ + Presses a button. + """ + key = "press" + aliases = ["press button", "button", "push", "push button"] + locks = "cmd:attr(crumbling_wall_found_button) and not locattr(is_dark)" # only accessible if the button was found and there is light. + help_category = "TutorialWorld" + + def func(self): + "Implements the command" + + if self.caller.db.crumbling_wall_found_exit: + # we already pushed the button + self.caller.msg("The button folded away when the secret passage opened. You cannot push it again.") + return + + # pushing the button + string = "You move your fingers over the suspicious depression, then gives it a " + string += "decisive push. First nothing happens, then there is a rumble and a hidden " + string += "{wpassage{n opens, dust and pebbles rumbling as part of the wall moves aside." + + # we are done - this will make the exit traversable! + self.caller.db.crumbling_wall_found_exit = True + # this will make it into a proper exit + eloc = self.caller.search(self.obj.db.destination, global_search=True) + if not eloc: + self.caller.msg("The exit leads nowhere, there's just more stone behind it ...") + return + self.obj.destination = eloc + self.caller.msg(string) + +class CmdSetCrumblingWall(CmdSet): + "Group the commands for crumblingWall" + key = "crumblingwall_cmdset" + def at_cmdset_creation(self): + "called when object is first created." + self.add(CmdShiftRoot()) + self.add(CmdPressButton()) + +class CrumblingWall(TutorialObject, Exit): + """ + The CrumblingWall can be examined in various + ways, but only if a lit light source is in the room. The traversal + itself is blocked by a traverse: lock on the exit that only + allows passage if a certain attribute is set on the trying + player. + + Important attribute + destination - this property must be set to make this a valid exit + whenever the button is pushed (this hides it as an exit + until it actually is) + """ + def at_object_creation(self): + "called when the object is first created." + super(CrumblingWall, self).at_object_creation() + + self.aliases = ["secret passage", "crack", "opening", "secret door"] + # this is assigned first when pushing button, so assign this at creation time! + self.db.destination = 2 + # locks on the object directly transfer to the exit "command" + self.locks.add("cmd:not locattr(is_dark)") + + self.db.tutorial_info = "This is an Exit with a conditional traverse-lock. Try to shift the roots around." + # the lock is important for this exit; we only allow passage if we "found exit". + self.locks.add("traverse:attr(crumbling_wall_found_exit)") + # set cmdset + self.cmdset.add(CmdSetCrumblingWall, permanent=True) + + # starting root positions. H1/H2 are the horizontally hanging roots, V1/V2 the + # vertically hanging ones. Each can have three positions: (-1, 0, 1) where + # 0 means the middle position. yellow/green are horizontal roots and red/blue vertical. + # all may have value 0, but never any other identical value. + self.db.root_pos = {"yellow":0, "green":0, "red":0, "blue":0} + + def _translate_position(self, root, ipos): + "Translates the position into words" + rootnames = {"red": "The {rreddish{n vertical-hanging root ", + "blue": "The thick vertical root with {bblue{n flowers ", + "yellow": "The thin horizontal-hanging root with {yyellow{n flowers ", + "green": "The weedy {ggreen{n horizontal root "} + vpos = {-1: "hangs far to the {wleft{n on the wall.", + 0: "hangs straight down the {wmiddle{n of the wall.", + 1: "hangs far to the {wright{n of the wall."} + hpos = {-1: "covers the {wupper{n part of the wall.", + 0: "passes right over the {wmiddle{n of the wall.", + 1: "nearly touches the floor, near the {wbottom{n of the wall."} + + if root in ("yellow", "green"): + string = rootnames[root] + hpos[ipos] + else: + string = rootnames[root] + vpos[ipos] + return string + + def return_appearance(self, caller): + "This is called when someone looks at the wall. We need to echo the current root positions." + if caller.db.crumbling_wall_found_button: + string = "Having moved all the roots aside, you find that the center of the wall, " + string += "previously hidden by the vegetation, hid a curious square depression. It was maybe once " + string += "concealed and made to look a part of the wall, but with the crumbling of stone around it," + string += "it's now easily identifiable as some sort of button." + else: + string = "The wall is old and covered with roots that here and there have permeated the stone. " + string += "The roots (or whatever they are - some of them are covered in small non-descript flowers) " + string += "crisscross the wall, making it hard to clearly see its stony surface.\n" + for key, pos in self.db.root_pos.items(): + string += "\n" + self._translate_position(key, pos) + self.db.desc = string + # call the parent to continue execution (will use desc we just set) + return super(CrumblingWall, self).return_appearance(caller) + + def at_after_traverse(self, traverser, source_location): + "This is called after we traversed this exit. Cleans up and resets the puzzle." + del traverser.db.crumbling_wall_found_button + del traverser.db.crumbling_wall_found_exit + self.reset() + + def at_failed_traverse(self, traverser): + "This is called if the player fails to pass the Exit." + traverser.msg("No matter how you try, you cannot force yourself through %s." % self.key) + + def reset(self): + "Called by tutorial world runner, or whenever someone successfully traversed the Exit." + self.location.msg_contents("The secret door closes abruptly, roots falling back into place.") + for obj in self.location.contents: + # clear eventual puzzle-solved attribues on everyone that didn't get out in time. They + # have to try again. + del obj.db.crumbling_wall_found_exit + + # Reset the roots with some random starting positions for the roots: + start_pos = [{"yellow":1, "green":0, "red":0, "blue":0}, + {"yellow":0, "green":0, "red":0, "blue":0}, + {"yellow":0, "green":1, "red":-1, "blue":0}, + {"yellow":1, "green":0, "red":0, "blue":0}, + {"yellow":0, "green":0, "red":0, "blue":1}] + self.db.root_pos = start_pos[random.randint(0, 4)] + self.destination = None + +#------------------------------------------------------------ +# +# Weapon - object type +# +# A weapon is necessary in order to fight in the tutorial +# world. A weapon (which here is assumed to be a bladed +# melee weapon for close combat) has three commands, +# stab, slash and defend. Weapons also have a property "magic" +# to determine if they are usable against certain enemies. +# +# Since Characters don't have special skills in the tutorial, +# we let the weapon itself determine how easy/hard it is +# to hit with it, and how much damage it can do. +# +#------------------------------------------------------------ + +class CmdAttack(Command): + """ + Attack the enemy. Commands: + + stab + slash + parry + + stab - (thrust) makes a lot of damage but is harder to hit with. + slash - is easier to land, but does not make as much damage. + parry - forgoes your attack but will make you harder to hit on next enemy attack. + + """ + + # this is an example of implementing many commands as a single command class, + # using the given command alias to separate between them. + + key = "attack" + aliases = ["hit","kill", "fight", "thrust", "pierce", "stab", "slash", "chop", "parry", "defend"] + locks = "cmd:all()" + help_category = "TutorialWorld" + + def func(self): + "Implements the stab" + + cmdstring = self.cmdstring + + + if cmdstring in ("attack", "fight"): + string = "How do you want to fight? Choose one of 'stab', 'slash' or 'defend'." + self.caller.msg(string) + return + + # parry mode + if cmdstring in ("parry", "defend"): + string = "You raise your weapon in a defensive pose, ready to block the next enemy attack." + self.caller.msg(string) + self.caller.db.combat_parry_mode = True + self.caller.location.msg_contents("%s takes a defensive stance" % self.caller, exclude=[self.caller]) + return + + if not self.args: + self.caller.msg("Who do you attack?") + return + target = self.caller.search(self.args.strip()) + if not target: + return + + string = "" + tstring = "" + ostring = "" + if cmdstring in ("thrust", "pierce", "stab"): + hit = float(self.obj.db.hit) * 0.7 # modified due to stab + damage = self.obj.db.damage * 2 # modified due to stab + string = "You stab with %s. " % self.obj.key + tstring = "%s stabs at you with %s. " % (self.caller.key, self.obj.key) + ostring = "%s stabs at %s with %s. " % (self.caller.key, target.key, self.obj.key) + self.caller.db.combat_parry_mode = False + elif cmdstring in ("slash", "chop"): + hit = float(self.obj.db.hit) # un modified due to slash + damage = self.obj.db.damage # un modified due to slash + string = "You slash with %s. " % self.obj.key + tstring = "%s slash at you with %s. " % (self.caller.key, self.obj.key) + ostring = "%s slash at %s with %s. " % (self.caller.key, target.key, self.obj.key) + self.caller.db.combat_parry_mode = False + else: + self.caller.msg("You fumble with your weapon, unable to choose an appropriate action...") + self.caller.location.msg_contents("%s fumbles with their weapon." % self.obj.key) + self.caller.db.combat_parry_mode = False + return + + if target.db.combat_parry_mode: + # target is defensive; even harder to hit! + hit *= 0.5 + + if random.random() <= hit: + self.caller.msg(string + "{gIt's a hit!{n") + target.msg(tstring + "{rIt's a hit!{n") + self.caller.location.msg_contents(ostring + "It's a hit!", exclude=[target,self.caller]) + + # call enemy hook + if hasattr(target, "at_hit"): + # should return True if target is defeated, False otherwise. + return target.at_hit(self.obj, self.caller, damage) + elif target.db.health: + target.db.health -= damage + if target.db.health <= 0: + # enemy is down! + return True + else: + return False + else: + # sorry, impossible to fight this enemy ... + self.caller.msg("The enemy seems unaffacted.") + return False + else: + self.caller.msg(string + "{rYou miss.{n") + target.msg(tstring + "{gThey miss you.{n") + self.caller.location.msg_contents(ostring + "They miss.", exclude=[target, self.caller]) + +class CmdSetWeapon(CmdSet): + "Holds the attack command." + def at_cmdset_creation(self): + "called at first object creation." + self.add(CmdAttack()) + +class Weapon(TutorialObject): + """ + This defines a bladed weapon. + + Important attributes (set at creation): + hit - chance to hit (0-1) + parry - chance to parry (0-1) + damage - base damage given (modified by hit success and type of attack) (0-10) + + """ + def at_object_creation(self): + "Called at first creation of the object" + super(Weapon, self).at_object_creation() + self.db.hit = 0.4 # hit chance + self.db.parry = 0.8 # parry chance + self.damage = 8.0 + self.magic = False + self.cmdset.add_default(CmdSetWeapon, permanent=True) + + def reset(self): + "When reset, the weapon is simply deleted, unless it has a place to return to." + if self.location.has_player and self.home == self.location: + self.location.msg_contents("%s suddenly and magically fades into nothingness, as if it was never there ..." % self.key) + self.delete() + else: + self.location = self.home + +#------------------------------------------------------------ +# +# Weapon rack - spawns weapons +# +#------------------------------------------------------------ + +class CmdGetWeapon(Command): + """ + Usage: + get weapon + + This will try to obtain a weapon from the container. + """ + key = "get" + aliases = "get weapon" + locks = "cmd:all()" + help_cateogory = "TutorialWorld" + + def func(self): + "Implement the command" + + rack_id = self.obj.db.rack_id + if eval("self.caller.db.%s" % rack_id): + # we don't allow to take more than one weapon from rack. + self.caller.msg("%s has no more to offer." % self.obj.name) + else: + dmg, name, aliases, desc, magic = self.obj.randomize_type() + new_weapon = create.create_object(Weapon, key=name, aliases=aliases,location=self.caller) + new_weapon.db.rack_id = rack_id + new_weapon.db.damage = dmg + new_weapon.db.desc = desc + new_weapon.db.magic = magic + ostring = self.obj.db.get_text + if not ostring: + ostring = "You pick up %s." + if '%s' in ostring: + self.caller.msg(ostring % name) + else: + self.caller.msg(ostring) + # tag the caller so they cannot keep taking objects from the rack. + exec("self.caller.db.%s = True" % rack_id) + + +class CmdSetWeaponRack(CmdSet): + "group the rack cmd" + key = "weaponrack_cmdset" + mergemode = "Replace" + def at_cmdset_creation(self): + self.add(CmdGetWeapon()) + +class WeaponRack(TutorialObject): + """ + This will spawn a new weapon for the player unless the player already has one from this rack. + + attribute to set at creation: + min_dmg - the minimum damage of objects from this rack + max_dmg - the maximum damage of objects from this rack + magic - if weapons should be magical (have the magic flag set) + get_text - the echo text to return when getting the weapon. Give '%s' to include the name of the weapon. + """ + def at_object_creation(self): + "called at creation" + self.cmdset.add_default(CmdSetWeaponRack, permanent=True) + self.rack_id = "weaponrack_1" + self.db.min_dmg = 1.0 + self.db.max_dmg = 4.0 + self.db.magic = False + + def randomize_type(self): + """ + this returns a random weapon + """ + min_dmg = float(self.db.min_dmg) + max_dmg = float(self.db.max_dmg) + magic = bool(self.db.magic) + dmg = min_dmg + random.random()*(max_dmg - min_dmg) + aliases = [self.db.rack_id, "weapon"] + if dmg < 1.5: + name = "Knife" + desc = "A rusty kitchen knife. Better than nothing." + elif dmg < 2.0: + name = "Rusty dagger" + desc = "A double-edged dagger with nicked edge. It has a wooden handle." + elif dmg < 3.0: + name = "Sword" + desc = "A rusty shortsword. It has leather wrapped around the handle." + elif dmg < 4.0: + name = "Club" + desc = "A heavy wooden club with some rusty spikes in it." + elif dmg < 5.0: + name = "Ornate Longsword" + aliases.extend(["longsword","ornate"]) + desc = "A fine longsword." + elif dmg < 6.0: + name = "Runeaxe" + aliases.extend(["rune","axe"]) + desc = "A single-bladed axe, heavy but yet easy to use." + elif dmg < 7.0: + name = "Broadsword named Thruning" + aliases.extend(["thruning","broadsword"]) + desc = "This heavy bladed weapon is marked with the name 'Thruning'. It is very powerful in skilled hands." + elif dmg < 8.0: + name = "Silver Warhammer" + aliases.append("warhammer") + desc = "A heavy war hammer with silver ornaments. This huge weapon causes massive damage." + elif dmg < 9.0: + name = "Slayer Waraxe" + aliases.extend(["waraxe","slayer"]) + desc = "A huge double-bladed axe marked with the runes for 'Slayer'. It has more runic inscriptions on its head, which you cannot decipher." + elif dmg < 10.0: + name = "The Ghostblade" + aliases.append("ghostblade") + desc = "This massive sword is large as you are tall. Its metal shine with a bluish glow." + else: + name = "The Hawkblade" + aliases.append("hawkblade") + desc = "White surges of magical power runs up and down this runic blade. The hawks depicted on its hilt almost seems to have a life of their own." + if dmg < 9 and magic: + desc += "\nThe metal seems to glow faintly, as if imbued with more power than what is immediately apparent." + return dmg, name, aliases, desc, magic diff --git a/contrib/tutorial_world/rooms.py b/contrib/tutorial_world/rooms.py new file mode 100644 index 0000000000..14d66fd469 --- /dev/null +++ b/contrib/tutorial_world/rooms.py @@ -0,0 +1,677 @@ +""" + +Room Typeclasses for the TutorialWorld. + +""" + +import random +from src.commands.cmdset import CmdSet +from src.utils import create, utils +from src.objects.models import ObjectDB +from game.gamesrc.scripts.basescript import Script +from game.gamesrc.commands.basecommand import Command +from game.gamesrc.objects.baseobjects import Room + +from contrib.tutorial_world import scripts as tut_scripts +from contrib.tutorial_world.objects import LightSource, TutorialObject + +#------------------------------------------------------------ +# +# Tutorial room - parent room class +# +# This room is the parent of all rooms in the tutorial. +# It defines a tutorial command on itself (available to +# all who is in a tutorial room). +# +#------------------------------------------------------------ + +class CmdTutorial(Command): + """ + Get help during the tutorial + + Usage: + tutorial [obj] + + This command allows you to get behind-the-scenes info + about an object or the current location. + + """ + key = "tutorial" + aliases = ["tut"] + locks = "cmd:all()" + help_category = "TutorialWorld" + + def func(self): + """ + All we do is to scan the current location for an attribute + called `tutorial_info` and display that. + """ + + caller = self.caller + + if not self.args: + target = self.obj # this is the room object the command is defined on + else: + target = caller.search(self.args.strip()) + if not target: + return + helptext = target.db.tutorial_info + if helptext: + caller.msg("{G%s{n" % helptext) + else: + caller.msg("{RSorry, there is no tutorial help available here.{n") + +class TutorialRoomCmdSet(CmdSet): + "Implements the simple tutorial cmdset" + key = "tutorial_cmdset" + def at_cmdset_creation(self): + "add the tutorial cmd" + self.add(CmdTutorial()) + +class TutorialRoom(Room): + """ + This is the base room type for all rooms in the tutorial world. + It defines a cmdset on itself for reading tutorial info about the location. + """ + def at_object_creation(self): + "Called when room is first created" + self.db.tutorial_info = "This is a tutorial room. It allows you to use the 'tutorial' command." + self.cmdset.add_default(TutorialRoomCmdSet) + + def reset(self): + "Can be called by the tutorial runner." + pass + + + +#------------------------------------------------------------ +# +# Weather room - scripted room +# +# The weather room is called by a script at +# irregular intervals. The script is generally useful +# and so is split out into tutorialworld.scripts. +# +#------------------------------------------------------------ + + +class WeatherRoom(TutorialRoom): + """ + This should probably better be called a rainy room... + + This sets up an outdoor room typeclass. At irregular intervals, + the effects of weather will show in the room. Outdoor rooms should + inherit from this. + + """ + def at_object_creation(self): + "Called when object is first created." + super(WeatherRoom, self).at_object_creation() + + # we use the imported IrregularEvent script + self.scripts.add(tut_scripts.IrregularEvent) + self.db.tutorial_info = \ + "This room has a Script running that has it echo a weather-related message at irregular intervals." + def update_irregular(self): + "create a tuple of possible texts to return." + strings = ( + "The rain coming down from the iron-grey sky intensifies.", + "A gush of wind throws the rain right in your face. Despite your cloak you shiver.", + "The rainfall eases a bit and the sky momentarily brightens.", + "For a moment it looks like the rain is slowing, then it begins anew with renewed force.", + "The rain pummels you with large, heavy drops. You hear the rumble of thunder in the distance.", + "The wind is picking up, howling around you, throwing water droplets in your face. It's cold.", + "Bright fingers of lightning flash over the sky, moments later followed by a deafening rumble.", + "It rains so hard you can hardly see your hand in front of you. You'll soon be drenched to the bone.", + "Lightning strikes in several thundering bolts, striking the trees in the forest to your west.", + "You hear the distant howl of what sounds like some sort of dog or wolf.", + "Large clouds rush across the sky, throwing their load of rain over the world.") + + # get a random value so we can select one of the strings above. Send this to the room. + irand = random.randint(0, 15) + if irand > 10: + return # don't return anything, to add more randomness + self.msg_contents("{w%s{n" % strings[irand]) + + +#----------------------------------------------------------------------------------- +# +# Dark Room - a scripted room +# +# This room limits the movemenets of its denizens unless they carry a and active +# LightSource object (LightSource is defined in tutorialworld.objects.LightSource) +# +#----------------------------------------------------------------------------------- + +class CmdLookDark(Command): + """ + Look around in darkness + + Usage: + look + + Looks in darkness + """ + key = "look" + aliases = ["l", 'feel', 'feel around', 'fiddle'] + locks = "cmd:all()" + help_category = "TutorialWorld" + + def func(self): + "Implement the command." + caller = self.caller + # we don't have light, grasp around blindly. + messages = ("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!", + "You don't see a thing! Blindly grasping the air around you, you find nothing.", + "It's totally dark here. You almost stumble over some un-evenness in the ground.", + "You are completely blind. For a moment you think you hear someone breathing nearby ... \n ... surely you must be mistaken.", + "Blind, you think you find some sort of object on the ground, but it turns out to be just a stone.", + "Blind, you bump into a wall. The wall seems to be covered with some sort of vegetation, but its too damp to burn.", + "You can't see anything, but the air is damp. It feels like you are far underground.") + irand = random.randint(0, 10) + if irand < len(messages): + caller.msg(messages[irand]) + else: + # check so we don't already carry a lightsource. + carried_lights = [obj for obj in caller.contents if utils.inherits_from(obj, LightSource)] + if carried_lights: + string = "You don't want to stumble around in blindness anymore. You already found what you need. Let's get light already!" + caller.msg(string) + return + #if we are lucky, we find the light source. + lightsources = [obj for obj in self.obj.contents if utils.inherits_from(obj, LightSource)] + if lightsources: + lightsource = lightsources[0] + else: + # create the light source from scratch. + lightsource = create.create_object(LightSource, key="torch") + lightsource.location = caller + string = "Your fingers bump against a piece of wood in a corner. Smelling it you sense the faint smell of tar. A {c%s{n!" + string += "\nYou pick it up, holding it firmly. Now you just need to {wlight{n it using the flint and steel you carry with you." + caller.msg(string % lightsource.key) + +class CmdDarkHelp(Command): + """ + Help command for the dark state. + """ + key = "help" + locks = "cmd:all()" + help_category = "TutorialWorld" + def func(self): + "Implements the help command." + string = "Can't help you until you find some light! Try feeling around for something to burn." + string += " You cannot give up even if you don't find anything right away." + self.caller.msg(string) + +# the nomatch system command will give a suitable error when we cannot find the normal commands. +from src.commands.default.syscommands import CMD_NOMATCH + +class CmdDarkNoMatch(Command): + "This is called when there is no match" + key = CMD_NOMATCH + locks = "cmd:all()" + def func(self): + "Implements the command." + self.caller.msg("Until you find some light, there's not much you can do. Try feeling around.") + +class DarkCmdSet(CmdSet): + "Groups the commands." + key = "darkroom_cmdset" + mergetype = "Replace" # completely remove all other commands + def at_cmdset_creation(self): + "populates the cmdset." + self.add(CmdTutorial()) + self.add(CmdLookDark()) + self.add(CmdDarkHelp()) + self.add(CmdDarkNoMatch()) + +# +# Darkness room two-state system +# + +class DarkState(Script): + """ + The darkness state is a script that keeps tabs on when + a player in the room carries an active light source. It places + a new, very restrictive cmdset (DarkCmdSet) on all the players + in the room whenever there is no light in it. Upon turning on + a light, the state switches off and moves to LightState. + """ + def at_script_creation(self): + "This setups the script" + self.key = "tutorial_darkness_state" + self.desc = "A dark room" + self.persistent = True + def at_start(self): + "called when the script is first starting up." + for char in [char for char in self.obj.contents if char.has_player]: + if char.is_superuser: + char.msg("You are Superuser, so you are not affected by the dark state.") + else: + char.cmdset.add(DarkCmdSet) + char.msg("The room is pitch dark! You are likely to be eaten by a Grue.") + def is_valid(self): + "is valid only as long as noone in the room has lit the lantern." + return not self.obj.is_lit() + def at_stop(self): + "Someone turned on a light. This state dies. Switch to LightState." + for char in [char for char in self.obj.contents if char.has_player]: + char.cmdset.delete(DarkCmdSet) + self.obj.db.is_dark = False + self.obj.scripts.add(LightState) + +class LightState(Script): + """ + This is the counterpart to the Darkness state. It is active when the lantern is on. + """ + def at_script_creation(self): + "Called when script is first created." + self.key = "tutorial_light_state" + self.desc = "A room lit up" + self.persistent = True + def is_valid(self): + "This state is only valid as long as there is an active light source in the room." + return self.obj.is_lit() + def at_stop(self): + "Light disappears. This state dies. Return to DarknessState." + self.obj.db.is_dark = True + self.obj.scripts.add(DarkState) + +class DarkRoom(TutorialRoom): + """ + A dark room. This tries to start the DarkState script on all + objects entering. The script is responsible for making sure it is + valid (that is, that there is no light source shining in the room). + """ + def is_lit(self): + """ + Helper method to check if the room is lit up. It checks all + characters in room to see if they carry an active object of + type LightSource. + """ + return any([any([True for obj in char.contents + if utils.inherits_from(obj, LightSource) and obj.is_active]) + for char in self.contents if char.has_player]) + + def at_object_creation(self): + "Called when object is first created." + super(DarkRoom, self).at_object_creation() + self.db.tutorial_info = "This is a room with custom command sets on itself." + # this variable is set by the scripts. It makes for an easy flag to look for + # by other game elements (such as the crumbling wall in the tutorial) + self.db.is_dark = True + # the room starts dark. + self.scripts.add(DarkState) + + def at_object_receive(self, character, source_location): + "Called when an object enters the room. We crank the wheels to make sure scripts are synced." + if character.has_player: + if not self.is_lit() and not character.is_superuser: + character.cmdset.add(DarkCmdSet) + if character.db.health and character.db.health <= 0: + # heal character coming here from being defeated by mob. + health = character.db.health_max + if not health: + health = 20 + character.db.health = health + self.scripts.validate() + + def at_object_leave(self, character, target_location): + "In case people leave with the light, we make sure to update the states accordingly." + character.cmdset.delete(DarkCmdSet) # in case we are teleported away + self.scripts.validate() + +#------------------------------------------------------------ +# +# Teleport room - puzzle room +# +# This is a sort of puzzle room that requires a certain +# attribute on the entering character to be the same as +# an attribute of the room. If not, the character will +# be teleported away to a target location. This is used +# by the Obelisk - grave chamber puzzle, where one must +# have looked at the obelisk to get an attribute set on +# oneself, and then pick the grave chamber with the +# matching imagery for this attribute. +# +#------------------------------------------------------------ + +class TeleportRoom(TutorialRoom): + """ + Teleporter - puzzle room. + + 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 + + """ + def at_object_creation(self): + "Called at first creation" + super(TeleportRoom, self).at_object_creation() + # what character.db.puzzle_clue must be set to, to avoid teleportation. + self.db.puzzle_value = 1 + # the target of the success teleportation. Can be a dbref or a unique room name. + self.db.success_teleport_to = "treasure room" + # the target of the failure teleportation. + self.db.failure_teleport_to = "dark cell" + + def at_object_receive(self, character, source_location): + "This hook is called by the engine whenever the player is moved into this room." + if not character.has_player or character.is_superuser: + # 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 + + results = ObjectDB.objects.object_search(teleport_to, global_search=True) + if not results or len(results) > 1: + # 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: + # superusers don't get teleported + character.msg("Superuser block: You would have been teleported to %s." % results[0]) + return + # teleport + character.execute_cmd("look") + character.location = results[0] # stealth move + character.location.at_object_receive(character, self) + +#------------------------------------------------------------ +# +# 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): + """ + Try to cross the bridge eastwards. + """ + key = "east" + aliases = ["e"] + locks = "cmd:all()" + help_category = "TutorialWorld" + + def func(self): + "move forward" + 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 = ObjectDB.objects.object_search(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 + caller.execute_cmd("look") + +# go back across the bridge +class CmdWest(Command): + """ + Go back across the bridge westwards. + """ + key = "west" + aliases = ["w"] + locks = "cmd:all()" + help_category = "TutorialWorld" + + def func(self): + "move forward" + 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 = ObjectDB.objects.object_search(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 + caller.execute_cmd("look") + +class CmdLookBridge(Command): + """ + looks around at the bridge. + """ + key = 'look' + aliases = ["l"] + locks = "cmd:all()" + help_category = "TutorialWorld" + + def func(self): + "Looking around, including a chance to fall." + bridge_position = self.caller.db.tutorial_bridge_position + + + 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 ...") + 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.") + message = "{c%s{n\n" % self.obj.key + messages[bridge_position] + "\n" + moods[random.randint(0, len(moods) - 1)] + 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.2 and not self.caller.is_superuser: + # we fall on 20% of the times. + fexit = ObjectDB.objects.object_search(self.obj.db.fall_exit) + if fexit: + string = "\n Suddenly the plank you stand on gives way under your feet! You fall!" + string += "\n You try to grab hold of an adjoining plank, but all you manage to do is to " + string += "divert your fall westwards, towards the cliff face. This is going to hurt ... " + string += "\n ... The world goes dark ...\n" + # note that we move silently so as to not call look hooks (this is a little trick to leave + # the player with the "world goes dark ..." message, giving them ample time to read it. They + # have to manually call look to find out their new location). Thus we also call the + # at_object_leave hook manually (otherwise this is done by move_to()). + self.caller.msg("{r%s{n" % string) + self.obj.at_object_leave(self.caller, fexit) + self.caller.location = fexit[0] # stealth move, without any other hook calls. + +# custom help command +class CmdBridgeHelp(Command): + """ + Overwritten help command + """ + 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): + self.add(CmdTutorial()) + self.add(CmdEast()) + self.add(CmdWest()) + self.add(CmdLookBridge()) + self.add(CmdBridgeHelp()) + +class BridgeRoom(TutorialRoom): + """ + 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 variabled stored on the player when entering and giving + special move commands will increase/decrease the counter until the bridge is crossed. + + """ + def at_object_creation(self): + "Setups the room" + super(BridgeRoom, self).at_object_creation() + + # at irregular intervals, this will call self.update_irregular() + self.scripts.add(tut_scripts.IrregularEvent) + # 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) + + self.db.tutorial_info = \ + """The bridge seem large but is actually only a single room that assigns custom west/east commands.""" + + def update_irregular(self): + """ + This is called at irregular intervals and makes the passage + over the bridge a little more interesting. + """ + strings = ( + "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.") + self.msg_contents("{w%s{n" % strings[random.randint(0, 7)]) + + 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 = ObjectDB.objects.object_search(self.db.west_exit) + eexit = ObjectDB.objects.object_search(self.db.east_exit) + fexit = ObjectDB.objects.object_search(self.db.fall_exit) + if not wexit or not eexit or not 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]: + character.db.tutorial_bridge_position = 4 + else: + 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 + + +#----------------------------------------------------------- +# +# 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_receive(self, character, source_location): + """ + Assign properties on characters + """ + + # setup + health = self.db.char_health + if not health: + health = 20 + + if character.has_player: + character.db.health = health + character.db.health_max = health + + if character.is_superuser: + string = "-"*78 + string += "\nWARNING: YOU ARE PLAYING AS A SUPERUSER (%s). TO EXPLORE NORMALLY YOU NEED " % character.key + string += "\nTO CREATE AND LOG IN AS A REGULAR USER INSTEAD. IF YOU CONTINUE, KNOW THAT " + string += "\nMANY FUNCTIONS AND PUZZLES WILL IGNORE THE PRESENCE OF A SUPERUSER.\n" + string += "-"*78 + character.msg("{r%s{n" % string) + +#------------------------------------------------------------ +# +# Outro room - unique room +# +# Cleans up the character from all tutorial-related properties. +# +#------------------------------------------------------------ + +class OutroRoom(TutorialRoom): + """ + Outro room. + """ + + def at_object_receive(self, character, source_location): + """ + Do cleanup. + """ + if character.has_player: + del character.db.health + del character.db.has_climbed + 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() diff --git a/contrib/tutorial_world/scripts.py b/contrib/tutorial_world/scripts.py new file mode 100644 index 0000000000..14cd9d6413 --- /dev/null +++ b/contrib/tutorial_world/scripts.py @@ -0,0 +1,109 @@ +""" +This defines some generally useful scripts for the tutorial world. +""" + +import random +from game.gamesrc.scripts.basescript import Script + +#------------------------------------------------------------ +# +# IrregularEvent - script firing at random intervals +# +# This is a generally useful script for updating +# objects at irregular intervals. This is used by as diverse +# entities as Weather rooms and mobs. +# +# +# +#------------------------------------------------------------ + +class IrregularEvent(Script): + """ + This script, which should be tied to a particular object upon + instantiation, calls update_irregular on the object at random + intervals. + """ + def at_script_creation(self): + "This setups the script" + + self.key = "update_irregular" + self.desc = "Updates at irregular intervals" + self.interval = random.randint(30, 70) # interval to call. + self.start_delay = True # wait at least self.interval seconds before calling at_repeat the first time + self.persistent = True + + # this attribute determines how likely it is the + # 'update_irregular' method gets called on self.obj (value is + # 0.0-1.0 with 1.0 meaning it being called every time.) + self.db.random_chance = 0.2 + + def at_repeat(self): + "This gets called every self.interval seconds." + rand = random.random() + if rand <= self.db.random_chance: + try: + self.obj.update_irregular() + except Exception: + pass + +class FastIrregularEvent(IrregularEvent): + "A faster updating irregular event" + def at_script_creation(self): + super(FastIrregularEvent, self).at_script_creation() + self.interval = 5 # every 5 seconds, 1/5 chance of firing + + +#------------------------------------------------------------ +# +# Tutorial world Runner - root reset timer for TutorialWorld +# +# This is a runner that resets the world +# +#------------------------------------------------------------ + +# # +# # This sets up a reset system -- it resets the entire tutorial_world domain +# # and all objects inheriting from it back to an initial state, MORPG style. This is useful in order for +# # different players to explore it without finding things missing. +# # +# # Note that this will of course allow a single player to end up with multiple versions of objects if +# # they just wait around between resets; In a real game environment this would have to be resolved e.g. +# # with custom versions of the 'get' command not accepting doublets. +# # + +# # setting up an event for reseting the world. + +# UPDATE_INTERVAL = 60 * 10 # Measured in seconds + + +# #This is a list of script parent objects that subscribe to the reset functionality. +# RESET_SUBSCRIBERS = ["examples.tutorial_world.p_weapon_rack", +# "examples.tutorial_world.p_mob"] + +# class EventResetTutorialWorld(Script): +# """ +# This calls the reset function on all subscribed objects +# """ +# def __init__(self): +# super(EventResetTutorialWorld, self).__init__() +# self.name = 'reset_tutorial_world' +# #this you see when running @ps in game: +# self.description = 'Reset the tutorial world .' +# self.interval = UPDATE_INTERVAL +# self.persistent = True + +# def event_function(self): +# """ +# This is called every self.interval seconds. +# """ +# #find all objects inheriting the subscribing parents +# for parent in RESET_SUBSCRIBERS: +# objects = Object.objects.global_object_script_parent_search(parent) +# for obj in objects: +# try: +# obj.scriptlink.reset() +# except: +# logger.log_errmsg(traceback.print_exc()) + + +