Writing:Working the Example

From Seltanikor
Jump to navigationJump to search
Writing
Introduction

Your First Age

Basic Concepts

Working the Example

The Ways of Printing

Assorted Features

Reference Section

The Scripting Language

Best Practices

Code Samples

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 or Event 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]