Added TutorialWorld - a small, but complete single-player quest area showing off some of Evennia's features. You can find it in contrib/tutorial_world.
Build with: @batchcommand contrib.tutorial_world.build I have tested it, but there are most likely still bugs, so report all you find (along with possible suggestions for improvements) to the bugtracker/mailing list.
This commit is contained in:
parent
8770a263ac
commit
b2b743b0aa
7 changed files with 3133 additions and 0 deletions
107
contrib/tutorial_world/README
Normal file
107
contrib/tutorial_world/README
Normal file
|
|
@ -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!
|
||||||
0
contrib/tutorial_world/__init__.py
Normal file
0
contrib/tutorial_world/__init__.py
Normal file
991
contrib/tutorial_world/build.ev
Normal file
991
contrib/tutorial_world/build.ev
Normal file
|
|
@ -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
|
||||||
347
contrib/tutorial_world/mob.py
Normal file
347
contrib/tutorial_world/mob.py
Normal file
|
|
@ -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)
|
||||||
902
contrib/tutorial_world/objects.py
Normal file
902
contrib/tutorial_world/objects.py
Normal file
|
|
@ -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 <object>
|
||||||
|
"""
|
||||||
|
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 <color> <direction>
|
||||||
|
if not len(self.arglist) > 1:
|
||||||
|
self.caller.msg("You must define which colour of root you want to move, and in which direction.")
|
||||||
|
return
|
||||||
|
color = self.arglist[0].lower()
|
||||||
|
direction = self.arglist[1].lower()
|
||||||
|
# get current root positions dict
|
||||||
|
root_pos = self.obj.db.root_pos
|
||||||
|
|
||||||
|
if 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 <enemy>
|
||||||
|
slash <enemy>
|
||||||
|
parry
|
||||||
|
|
||||||
|
stab - (thrust) makes a lot of damage but is harder to hit with.
|
||||||
|
slash - is easier to land, but does not make as much damage.
|
||||||
|
parry - forgoes your attack but will make you harder to hit on next enemy attack.
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
|
# this is an example of implementing many commands as a single command class,
|
||||||
|
# using the given command alias to separate between them.
|
||||||
|
|
||||||
|
key = "attack"
|
||||||
|
aliases = ["hit","kill", "fight", "thrust", "pierce", "stab", "slash", "chop", "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
|
||||||
677
contrib/tutorial_world/rooms.py
Normal file
677
contrib/tutorial_world/rooms.py
Normal file
|
|
@ -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()
|
||||||
109
contrib/tutorial_world/scripts.py
Normal file
109
contrib/tutorial_world/scripts.py
Normal file
|
|
@ -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())
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue