Writing:Working the Example
Writing |
---|
Introduction |
Back to Dusty Hill. We can do much more with this world.
Moving around
Update the desc
of the location to look like this:
You are standing at the foot of a hill in the middle of a dusty steppe. Stands of wicker-dry [grass] dot the landscape, rattling thinly in the breeze. A [trail] leads upwards.
Now add a new property, called trail
. Change its pop-up menu from Text
to Move
.
The trail
property line now has four entries. For now, we’ll fill in just the Destination
field; set it to hilltop
and leave the other three lines alone. Save this.
When you click on the “trail” link (in the game), you’ll see an error “No such location: hilltop”. Because, of course, there isn’t. Let’s fix this.
Add a second location. (Remember, you do this from the Dusty Hill
world screen.) Give it the name Top of Hill
, the key hilltop
(lower case!), and this description:
You are on top of a rocky hill, overlooking a plain of sparse yellow grass and not much besides. A rough [trail] leads downwards.
Now the “trail” link at the foot of the hill works – it brings you to the top.
However, the “trail” link at the top does not yet work. Remember, the properties you’ve created so far are attached to the foot
location; they’re not accessible from anywhere else. Add a new trail
property in the hilltop
screen to remedy this. The destination line should be foot
.
To make life more interesting, add this text on the (Move message)
line:
You make your way back down the hill.
This message appears to the player (in the event pane) when he clicks the move link. To keep the balance, add an appropriate hill-climbing message to the trail
property in the foot
location.
The third and fourth lines of the move property are messages to other players; one to the players you are departing from, and one to the players you are heading towards. You don’t have to fill in these lines, because the system provides simple defaults. But you might want to provide a colorful customization, perhaps “[$name] trudges wearily up towards the distant summit.”
(As you’ve probably guessed, [$name]
is a magic token which is filled in with the acting player’s name. We’ll get to names and other pronoun magic later.)
Tidier hyperlinks
In my sample Ages, I’ve tried to make my travel hyperlinks longer, and include a clear description of the motion involved. For example:
You are standing at the foot of a hill in the middle of a dusty steppe. Stands of wicker-dry [grass] dot the landscape, rattling thinly in the breeze. A [trail leads upwards].
Changing the text breaks the hyperlink, because the Move
property is still named trail
. How to fix it?
We’d like to rename the property to “trail leads upwards”, but I said earlier that a property name cannot contain spaces. But try it anyway! You’ll find that the spaces are converted to underscores: trail_leads_upwards
. And indeed this works. By default, the text of a hyperlink is converted into a property name which is lowercase, with spaces and punctuation replaced by underscores.
(We call this “sluggification”, because... (blah blah blah long explanation...) Linotype type-casting machines. Never mind. The exact sluggification rules are a bit tricky. You can experiment if you like. The ultimate goal is to generate a valid Python identifier that looks like the original text.)
But what if we don’t want that long property name? It’s awkward, and prone to breakage if we happen to update the room description again.
Change the property name back to trail
, and then update the desc
to this:
You are standing at the foot of a hill in the middle of a dusty steppe. Stands of wicker-dry [grass] dot the landscape, rattling thinly in the breeze. A [trail leads upwards|trail].
The vertical bar |
indicates an explicit property name. The text before the bar is displayed to the player; the text after the bar is the property name to invoke: trail
. No sluggification is performed here, so be sure to use lower-case after the bar.
Here’s another handy shortcut:
You are standing at the foot of a hill in the middle of a dusty steppe. Stands of [wicker-dry||grass] dot the landscape, rattling thinly in the breeze. A [trail leads upwards|trail].
The double bar in [wicker-dry||grass]
means that the whole text is displayed, but only the last part (grass
) is used to invoke the property. (The text after the double bar is sluggified.) This is a common pattern, when you want a long descriptive link but a short property name.
The linking page
Let’s go back to that “hazy destination” message, which is what appears if you look at the “Dusty Hill” entry in your linking booklet.
Add a new property in the foot
location, with the name portaldesc
and the text:
The page shows a dry, rocky hillside.
portaldesc
is another special property. Add it to any location that you plan to use as a link-in spot. It is shown as the “linking page image.”
(A warning: unlike most text properties, portaldesc
cannot use magic square-bracket tokens like [$name]
or the other interpolations we’ll talk about soon. If you try to put them in, they won’t behave magically; the player will see the square brackets as-is.)
Events
Not all actions involve walking around. At the top of the hill, set this description:
You are on top of a rocky hill, overlooking a plain of sparse yellow grass and not much besides. A [rough trail leads downwards|trail].
A single dead tree stands near the summit – a [grey, hollowed-out||bole] without branch or leaf.
This is two paragraphs of text in the same property. Be sure to enter it that way! They must have a blank line between them, to mark a paragraph break – that is, you should hit Enter twice. (If you just hit Enter once, the paragraphs will flow together.)
Create a bole
property in this location. (Remember, the double bar ||
in the link text indicates that the whole string will be displayed to the player, but only last part names a property.)
Using the pop-up menu, change the type of the bole
property to Event
. Now you see two text fields: a message to display to the player, and a message to display to other players in the same location. These messages will appear in the event pane. Try this line in the first field:
You give the hollow tree a good thump. A resonant booming sound rolls down the hill.
Click the link in the game to observe the effect.
Most events are visible to every nearby player. (Or, in this case, audible!) So it’s usually worth filling in the second “other players” field of the Event
property. Try this:
[$name] gives the hollow tree a thump with [$their] hand.
You won’t be able to test this message, since you’ve only got one character in the world. It does what you think, though.
We’ve seen the [$name]
token before. [$their]
is a pronoun token; it gets filled in with “his”, “her”, “its” as appropriate. (Yes, it’s third-person singular. Use [$our]
if it really bugs you.)
Interpolation and assignment
Extend the second line of desc
to look like this:
A single dead tree stands near the summit – a [grey, hollowed-out||bole] without branch or leaf. A bit of chalk lies at the tree’s base, and someone has used it to scrawl: “[[count]] people have been here.”
(From now on, I’ll just be giving the second paragraph of the desc
property. The first paragraph, with the trail link, will stay the same. Be sure not to erase it, or you’ll be stuck on top of the hill...)
Note that [[count]]
is in double square brackets. This token disappears in the game – the display reads “ people have been here,” which is hardly informative. That’s because it’s looking for a count
property to insert, and we haven’t defined one.
Do that now; create a property called count
whose value is 0
. As soon as you save it, the location in the game will update to show the value: “0 people have been here.” Notice that double square brackets don’t produce a hyperlink; instead, they look up and insert the named property as-is. We call this “interpolation”, if we’re in a mood for three-bit words.
No, you can’t insert a
Move
orEvent
property into a room description this way. That would be silly. Only texts and other static values can be interpolated.
This isn’t an interesting example until the count
property changes. You can have fun editing it in the build window, but really the point is for players to change it. So try this:
A single dead tree stands near the summit – a [grey, hollowed-out||bole] without branch or leaf. A bit of [chalk|count=count+1] lies at the tree’s base, and someone has used it to scrawl: “[[count]] people have been here.”
The word “chalk” is now in single brackets, which makes it a hyperlink. However, the target of the link (after the bar) isn’t a property; it’s a line of code. As you might expect, this increases the count
property...
...Or not. If you try it, you’ll get a truculent TypeError.
The problem here is that the count
property is text right now. The text string “0” is not a numeric 0. To fix this, change the type selector to Value
, and fill in the 0 value. Now the “chalk” link works; you can click away, increasing the displayed value each time.
Notice, by the way, that the value in the build screen does not change when you click the link. The property is still defined as 0, even though it appears as 9 or 27 or some such exuberant value in the game. Why?
If you answered “property inheritance,” you understood more of that earlier chapter than I feared! The first time you clicked, the count=count+1
statement read in the world value (zero), added one, and stored an instance value of 1. (The instance property is created as soon as a player action tries to store a property value.) Subsequent clicks increased this instance property.
If another player were standing in a different instance, she would still see the count as zero. And if she clicked the link, she would see her own instance-copy of the property increase.
The rule is: the build screen only shows the world definition, and player actions cannot change the world definition. Only the Writer can do that.
Conditional text
The chalk message is still quite primitive. Let’s fancy it up.
A single dead tree stands near the summit – a [grey, hollowed-out||bole] without branch or leaf. A bit of [chalk|count=count+1] [$if count]and an [eraser|del count][$end] lies at the tree’s base, and someone has used it to scrawl:
[$if count == 0]
“Nobody has been here.”
[$elif count == 1]
“One person has been here.”
[$else]
“[[count]] people have been here.”
[$end]
The new feature here is the [$if ...]
token, which lets us vary the text. In the first line, we’ve added “...and an eraser” (with another action link) but only if the count
property exists and is not zero. The code of the eraser link is del count
, which deletes the count
instance property and allows the original world property (zero) to shine through. So the eraser link actually erases itself, which is sort of cool.
The eraser link could have been
count=0
instead. This doesn’t do exactly the same thing, but the result would look the same to the player.
The written message is a more complicated [$if ...] [$elif ...] [$else] [$end]
sequence. Here we test an expression (count == 0
) rather than a bare property.
For clarity, I’ve written the text out on multiple lines. Remember that adjacent lines of text get reassembled into a paragraph, so you can always use single line breaks to make your code easier to read. (A double line break – that is, a blank line – is displayed as a true paragraph break.)
Code
If the player uses the chalk, it should generate an event message as well as updating the count
property.
It’s possible to jam multiple lines of code into a link, but the formatting gets ugly. Let’s use this opportunity to break the code out into a separate property.
A single dead tree stands near the summit – a [grey, hollowed-out||bole] without branch or leaf. A bit of [chalk|bumpcount] [$if count]and an [eraser|clearcount][$end] lies at the tree’s base, and someone has used it to scrawl:
[$if count == 0]
“Nobody has been here.”
[$elif count == 1]
“One person has been here.”
[$else]
“[[count]] people have been here.”
[$end]
Now create a bumpcount
property, set its type to Code
, and fill in these lines:
count = count+1 event("You grab the chalk and mark your presence on the tree.")
Clicking the link now increments the number and displays an event. The event()
function does what you think.
(If the link pops up a focus pane displaying the code, then you forgot to change the property type. Text
properties are displayed as focus close-ups; all other properties are executed.)
Event
properties permit both a message to the acting player and a message to everybody else. You can do the same thing with the event()
function:
count = count+1 event("You grab the chalk and mark your presence on the tree.", text("[$name] picks up the chalk and scrawls on the tree."))
The text()
function here ensures that "[$name] picks up the chalk and scrawls on the tree."
is interpreted as wiki text rather than a raw string. In a raw string, the [$name]
token would appear as-is to viewers. (We could apply text()
to the first argument too, but it’s not necessary, because there are no magic tokens there.)
I’ll leave the clearcount
property as an exercise.
One more example. We’ve seen that a link like [weathered sign|sign]
brings up a close-up view if sign
is a text property. You might conclude that sign
, as a statement by itself, does this job, and you’d be right. You could instead write [weathered sign|codesign]
, and have a one-line codesign
property:
sign
This accomplishes the same thing – popping up the sign
text property – in a more complicated way. Which looks silly, but you could extend the codesign
property to do more:
event("You peer at the sign.", text("[$name] peers at the sign.")) sign
Code arguments
The code above displays “one person” when count
is 1, but higher values appear as digits. Let‘s allow values up to ten to appear as English.
Create a property saycount
, and set its type to Code (args)
. The property will now act as a function with arguments.
The first field is the list of arguments; enter num
. This indicates that saycount
takes one argument, which will be given the temporary internal name num
. (If there were multiple arguments, they would be separated by commas.)
In the second field, enter the following:
_names = ['Zero', 'One', 'Two', 'Three', 'Four', 'Five', 'Six', 'Seven', 'Eight', 'Nine', 'Ten'] if num >= len(_names): return num else: return _names[num]
(The entries ‘Zero’ and ‘One’ won't actually be used in this example, but by filling the appropriate spaces, they make sure the number names align properly with the array indexes.)
Finally, edit desc
by replacing [[count]] people have been here.
with [[saycount(count)]] people have been here.
When this is displayed, the saycount
property will be called with count
as its argument.
Note that adding more number words is as easy as adding more entries to the end of the list – you don't need a separate elif for each case.
(This example is a bit artificial. We could have written saycount
as an ordinary Code
property, checking the value of count
directly. But by passing it as an argument, we’ve created a function which could be reused in several places in the Age.)
Timers
A rich environment may want to provide effects that happen “in the background” – either on a regular schedule, or as delayed reactions to player actions. Here is a simple example.
Go back to the foot of the hill. Add a second paragraph to desc
, as follows:
A rusty [pump||handle] sticks up from the ground nearby.
The handle
property should be Code
, containing these two lines:
event("You pump the handle hard, but only faint wheezing noises emerge.", text("[$name] pumps the handle.")) sched(5, pumptimer)
This won’t work until you add a second Code
property, called pumptimer
:
eventloc(locations.foot, "A few drops of water dribble from the pump.")
Always schedule a code property, rather than a line of code.
sched(5, eventloc(...))
will not do what you want.
When you click the pump handle now, you’ll see one event immediately, and a second event five seconds later. The sched()
function causes another code property to be invoked after a fixed delay. The delay is specified in seconds.
The eventloc()
function is like event()
, but it specifies the location where the event is displayed. (event()
triggers an event around the acting player, but there is no acting player in a timer event.) eventloc()
only takes a single argument; everyone in the specified location will see the same event message.
We’ve also introduced the locations.foot
notation, which specifies a location by its short name. You can also use this notation to access location properties. For example, locations.hilltop.count
is the count
property of the hilltop
.
This brings up an important warning about delayed code, which is that it does not run in a particular location. When the player triggers the handle
property in this example, it runs in the player’s location, so pumptimer
refers to a location property. But then when pumptimer
runs, it’s floating around in space. There is no “acting player”, so event()
won’t work – that’s why we use eventloc()
. And there is no current location. Symbols refer to realm properties rather than location properties.
So if you want your delayed code to set a location property, you’d have to write locations.hilltop.count=1
. Writing just count=1
would set the realm property count
.
(Planned feature: properties should run in the location where they are defined. This would be a sort of “lexical scope”, and I think it would be less confusing overall, although the details are odd.)
Repeating timers
You can also set up a repeating event:
sched(60, clockchime, repeat=True)
This invokes the clockchime
code property every sixty seconds, forever.
Except not actually forever. The server has a notion of which instances are “awake” – currently inhabited. Once every player has left an instance, the server waits ten minutes (in case someone comes back). After ten minutes uninhabited, the server puts the instance to “sleep”. All outstanding timers are cancelled. There will be no more activity in the instance until a player returns.
Usually you’ll want your repeating event to run all the time, as long as the instance is awake. You can set this up by creating a realm-level code property named on_wake
. Put your sched()
call in this, and it will be fired every time the instance is awakened.
A minor nuisance: if you set up an on_wake
property with the sched()
line above, and then test it, you’ll have trouble editing the clockchime
property. Once the timer is running, changes to the clockchime
property will seem to have no effect – the code is already loaded into the timer.
To work around this, put the instance to sleep and wake it up again. Of course the ten-minute wait is tedious, so there’s a special command to do this immediately. Type /bootinstance
in the chat pane, and the server will immediately kick you out and put the instance to sleep.
(You can only do this in worlds that you created!)
- The
on_wake
property is one type of hook: a specially-named property that the server invokes in certain circumstances. There are several of these, which are listed in the Reference Section.
Creating a bookshelf
The most distinctive feature of Myst’s Ages are the linking books. We’ll certainly want a way to include links in our own worlds.
This is a little more complicated than adding an action or exit. It’s a three-step process: you must create a portal list (bookshelf), add portals (links) to the list, and then create a PortList
property to display the list to the player.
Return to the world page in the build interface, and look at the “Portal lists” table. This shows all the bookshelves available in the world. (A standalone linking book is just a bookshelf with exactly one link.) Press the “New Portal List” button to create a new one.
You are now looking at an empty bookshelf. Set the key to mylist
. This string will be used later in a property to identify this list. (It’s rather like the key-name of a location.)
Now hit the “New Portal” button. You’ll get a new link... to the Information Booth in the starting world. Not very exciting! But this is just an initial placeholder.
Hit the “World” button on the left to change the link’s destination. The menu that appears will allow you to select any link you have in your personal booklet. The booklet is your “clipboard” for copying links around.
When you add a link this way, it copies the world, location, and scope from your linking booklet. So you may well wind up with a link to somebody else’s personal instance. You can stick with that, or change it. Press the “Instance” button to customize the destination instance.
The options here are a little confusing:
Player’s personal
: The link will always go to the player’s own personal instance.Player’s current
: The link will always go to the scope the player was in when they used it. If the player is in a global instance, the link will be to a global instance. If the player is in a personal instance, the link will be to the parallel personal instance. (Not necessarily the player’s personal instance!)Player’s global
: The link will always go to the global instance. (“Player’s global” is a confusing label, sorry.)Global
: The link will always go to the global instance. (Just like the previous option. There’s a minor difference in the database entry but the result is identical. I should drop one of them.)Personal: (you)
: The link will go to your personal instance (not the player’s!)
(The “Instance” button will be disabled if the target world has solo
or shared
instancing. By definition, a solo
world only has Player’s personal
available, and a shared
world only has Global
.)
(Planned feature: the ability to select a friend’s personal scope, if that player has given you access.)
(Planned feature: group scopes.)
Once you have the list set up the way you want, go to a location in your world and create a PortList
property. Set the key entry to mylist
, matching the list you just created. When the player looks at property, they’ll see your list.
The other lines of the PortList
customize the behavior of the shelf. The read access level defaults to visitor
, meaning that anybody can look at the contents of the shelf. The edit access level defaults to member
, which currently means that only the instance owner can add new books to the shelf.
The links you’ve added are part of the world definition, so only you can delete them. World-defined links appear in every instance. But the instance owner can add and remove additional links, which are specific to that instance.
The “Label” line will be displayed as a description of the bookshelf. You can use this to detail what the shelf looks like, or where it is in relation to the room. (This line may contain links.)
If you want to create a standalone book -- not part of a bookshelf -- you should create a list with exactly one link, and then set the property’s “Focus” line to true
. In this case, the “Label” line describes the book rather than the shelf. (But not the book’s linking image; that comes from the destination world’s portaldesc
property.)
If you want to create a public bookshelf, which anybody can add books to, create an empty portal list. Then set up a PropList
whose edit access is visitor
. (This is how the public shelf in the Seltani gallery works.)
Creating an age-wide booklet page
If you create a realm-level text property named instancepane
, its contents will appear as a special booklet page (in the right column) for players in that world.
The instancepane
can contain links and interpolations, like any text property. You can use this facility to offer the player information and controls that are available throughout the Age.
Any code in the property will execute at the realm level; that is, there will be no "current location". The "current player" will be set correctly, but location()
will return None
. (If you need this value, use location(player)
. Yes, this is an untoward nuisance.)
Note that the booklet page will only appear if the instancepane
exists and generates any text. So if you make the entire text conditional:
[$if condition]...[$end]
...then the booklet page will only appear when the condition is true. You might do this, to have a booklet page that only appears for privileged players:
[$if access.level(level=access.MEMBER)] ... [$end]