mirror of
https://github.com/evennia/evennia.git
synced 2026-04-02 22:17:17 +02:00
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