Custom Banner
Overview
This extension implements a CustomBannerWindow class that vastly eases the process of setting up banners (windows in the display) and displaying material in them. e.g. to set up a graphics banner to display pictures, starting with pic1.jpg at startup, but not appearing at all on an interpreter that can't display JPEGs you could define:
pictureWindow: CustomBannerWindow canDisplay = (systemInfo(SysInfoJpeg)) bannerArgs = [nil, BannerAfter, statuslineBanner, BannerTypeText, BannerAlignTop, 10, BannerSizeAbsolute, BannerStyleBorder] currentContents = '<img src="pic1.jpg">' ;
Then to change the picture dislayed at a later point, call:
pictureWindow.updateContents('<img src="pic2.jpg">');
And everything else, including getting everything right on RESTART, UNDO and RESTORE should be taken care of.
To use this extension, simply include the customBanner.t file in your project after the adv3Lite library but before your own game files. Note that the file banner.t (which forms part of the main adv3Lite library) must also be present in your project.
The BannerWindow Class
To understand the workins of the CustomBannerWindow class it may first be helpful to know a little about the BannerWindow class from which it descends and to which it provides a more convenient interface. The following description of this class is heavily based of Mike Roberts's comments in the banner.t file.
A "banner" is an independent window shown within the interpreter's main application display frame (which might be the entire screen on a character-mode terminal, or could be a window in a GUI system). The game can control the creation and destruction of banner windows, and can control their placement and size.
A BannerWindow corresponds to an on-screen banner. For each banner window a game wants to display, the game must create an object of this class.
Note that merely creating a BannerWindow object doesn't actually display a banner window. Once a BannerWindow is created, the game must call the object's showBanner() method to create the on-screen window for the banner.
BannerWindow instances are intended to be persistent (not transient). The banner manager keeps track of each banner window that's actually being displayed separately via an internal transient object; the game doesn't need to worry about these tracking objects, since the banner manager automatically handles them.
To display a Banner window we call its showBanner(parent, where, other, windowType, align, size, sizeUnits, styleFlags) method. The game should call this method when it first wants to display the banner.
- parent is the parent banner; this is an existing BannerWindow object. If 'parent' is nil, then the parent is the main game text window. The new window's display space is obtained by carving space out of the parent's area, according to the alignment and size values specified.
- where and other give the position of the banner among the children of the given parent. 'where' is one of the constants BannerFirst, BannerLast, BannerBefore, or BannerAfter. If where is BannerBefore or BannerAfter, other gives the BannerWindow object to be used as the reference point in the parent's child list; other is ignored in other cases. Note that other must always be another child of the same parent; if it's not, then we act as though where were given as BannerLast.
- windowType is a BannerTypeXxx constant giving the new window's type. This can be either BannerTypeText (for an ordinary text stream window) or BannerTypeTextGrid (for a text grid window)
- align is a BannerAlignXxx constant giving the alignment of the new window, which can be one of BannerAlignTop, BannerAlignBottom, BannerAlignLeft or BannerAlignRight.
- size is an integer giving the size of the banner in units specified by sizeUnits, which is a BannerSizeXxx constant (one of BannerSizePercent — size is a percentage of available space or BannerSizeAbsolute — size is natural units of window type). If size is nil, it indicates that the caller doesn't care about the size, usually because the caller will be resizing the banner soon anyway; the banner will initially have zero size in this case if we create a new window, or will retain the existing size if there's already a system window.
- styleFlags is a combination of BannerStyleXxx constants (),
combined with the bitwise OR operator, '|', giving the requested
display style of the new banner window. The possibilties are:
- BannerStyleBorder: banner has a visible border
- BannerStyleVScroll: vertical scrollbar
- BannerStyleHScroll: horizontal scrollbar
- BannerStyleAutoVScroll:automatic vertical scrolling
- BannerStyleAutoHScroll: automatic horizontal scrolling
- BannerStyleTabAlign: <TAB> alignment support
- BannerStyleMoreMode: use MORE mode
- BannerStyleHStrut: include in parent's auto width
- BannerStyleVStrut: include in parent's auto height
Note that if we already have a system banner window, and the existing banner window has the same characteristics as the new creation parameters, we'll simply re-use the existing window rather than closing and re-creating it; this reduces unnecessary redrawing in cases where the window isn't changing. If the caller explicitly wants to create a new window even if we already have a window, the caller should simply call removeBanner() before calling this routine.
The removeBanner() method removes the banner's on-screen window. The BannerWindow object itself remains valid, but after this method returns, the BannerWindow no longer has an associated display window. Note that any child banners of ours will become undisplayable after we're gone. A child banner depends upon its parent to obtain display space, so once the parent is gone, its children no longer have any way to obtain any display space. Our children remain valid objects even after we're closed, but they won't be visible on the display.
The CustomBannerWindow Class
A CustomBannerWindow, like a BannerWindow, corrsponds to an on-screen banner. The purpose of CustomBannerWindow is to eliminate most of the busy-work that a game author would otherwise have to take care of in displaying and manipulating banners.
As with BannerWinnow, merely creating a CustomBannerWindow does not display the banner. However, any CustomBannerWindows in existence at the start of the game will be added to the screen display, unless the condition specified in their shouldDisplay() method prevents initialization.
The one property that must be defined on each instance of a CustomBannerWindow is bannerArgs, which takes the form:
bannerArgs = [parent, where, other, windowType, align, size, sizeUnits, styleFlags]
where each list element has the same meaning as the corresponding argument to BannerWindow.showBanner()
This merely ensures that the CustomBannerWindow is added to the screen's banner window layout. To have the CustomBannerWindow display some content when first added to the screen layout, override its current contents property:
currentContents = 'My initial contents'
To change what's displayed in a CustomBannerWindow from game code, call its updateContents() method, e.g.:
pictureWindow.updateContents('<img src="pics/troll.jpg">');
To redisplay the current contents, call the showCurrentContents() method. By default a call to updateContents() or showCurrentContents() clears the window before displaying the new content. To have the additional content added to the existing content, change the clearBeforeUpdate property to nil.
You can control whether the game uses this banner at all by overriding the canDisplay property. The main purpose of this property is to easily allow a game to run on different types of interpreter. For example, if your banner is meant to display pictures in the JPEG format, there's no point having it included in the screen layout of an interpreter that can't display JPEGs, and attempts to update its contents with a new picture should do nothing. In which case we could define:
canDisplay = (systemInfo(SysInfoJpeg))
Calls to CustomBannerWindow methods like updateContents() and clearWindow() should be safe on an interpreter for which shouldDisplay returns nil, since by default these will do nothing beyond updating the banner's currentContents property. This makes it easier to write game code that is suitable for all classes of interpreter.
To have a CustomBannerWindow resize to contents each time its contents are displayed, set its autoSize property to true.
If you do not want a CustomBannerWindow you have defined not to be dispayed at game startup, set its isActive property to nil. Call the activate() method to add it to the screen layout and the deactivate() method to remove it, or any other CustomBannerWindow, from the screen display.
Obviously, it is then the game author's responsibility to ensure that no other banner window that's meant to persist after pictureWindow is deactivated depends on pictureWindow for its existence; i.e. that we're not deactivating the parent of some other banner window(s) we want to keep or subsequently activate, or the sibling of any banner window that's subsequently going to defined in relation to us.
Example Game
The following example game (used to test the adv3Lite port of the CustomBannerWindow extension) should help illustrate how it can be used in practice. It isn't much of a game; all you can do is walk through a small part of Oxford from Radcliffe Square to the dining hall in Harris Manchester College, but along the way you get to see pictures of the various locations you pass and of many of the landmarks you examine. The code assumes that these pictures are located in a pics directory beneath your game directory and that they have a jpg extension, so I'm afraid you won't see much without the pictures. If you're keen to try it out you can download them from here (I should warn you that they're not particularly good photos!).
#charset "us-ascii" #include <tads.h> #include "advlite.h" versionInfo: GameID IFID = 'c35ea1f5-d68b-46bb-9782-c1784d5fa4aa' name = 'Oxtour Lite' byline = 'by Eric Eve' htmlByline = 'by Eric Eve' version = '2' authorEmail = 'Eric Eve' desc = 'An adv3Lite port of the Oxtour Game to test the customBanner extension in adv3Lite.' htmlDesc = 'An adv3Lite port of the Oxtour Game to test the customBanner extension in adv3Lite.' showAbout() { aboutMenu.display(); } ; gameMain: GameMainDef /* Define the initial player character; this is compulsory */ initialPlayerChar = me /* Display a picture of our starting location at the start of the game. */ showIntro() { startRoom.showPic(); } ; /* Define a template for convenient definitions of objects of our custom Picture class. */ Picture template 'picFile' 'caption'; /* A custom class to display a picture of our choice. */ class Picture: object /* The name of the picture file we want to display. We'll automatically append .jpg to it. */ picFile = '' /* Tha caption we want to display for this picture . */ caption = '' /* Method to display this picture. */ showPic() { /* Ensure that all the windows we need are active. */ if(!picWindow.isActive) { centreWindow.activate(true); leftWindow.activate(true); picWindow.activate(true); rightWindow.activate(true); captionWindow.activate(true); } /* * Update the contents of our picture window to display the picture defined in our picFile * property. */ picWindow.updateContents(' <img src="pics/' + picFile + '.jpg" >'); /* Update our caption window to display the associated caption. */ captionWindow.updateContents('<- ' + caption); } ; /* * Banner Definitions * * We start by defining a banner window to occupy the centre of the screen between the status * window and the main game window. This will act as the parent of our other custom windows. */ centreWindow: CustomBannerWindow bannerArgs = [nil, BannerAfter, statuslineBanner, BannerTypeText, BannerAlignTop, 15, BannerSizeAbsolute, BannerStyleVStrut | BannerStyleBorder ] currentContents = '<body bgcolor=statusbg>' ; /* * We next define the four custom banner windows we want placed in our centre window, working from * left to right. Fist is simply a vertical border. */ leftWindow: CustomBannerWindow bannerArgs = [centreWindow, BannerFirst, nil, BannerTypeText, BannerAlignLeft, 1, BannerSizePercent, BannerStyleVStrut | BannerStyleBorder] currentContents = '<body bgcolor=statusbg>' ; /* * Next is the window for displaying pictures, which we align left against the previous window and * define to take up 50% of the available space. */ picWindow: CustomBannerWindow bannerArgs = [centreWindow, BannerAfter, leftWindow, BannerTypeText, BannerAlignLeft, 50, BannerSizePercent, BannerStyleBorder] autoSize = true ; /* Then we define another separator window to align left against our picture window. */ rightWindow: CustomBannerWindow bannerArgs = [centreWindow, BannerAfter, picWindow, BannerTypeText, BannerAlignLeft, 1, BannerSizePercent, BannerStyleVStrut | BannerStyleBorder] currentContents = '<body bgcolor=statusbg>' ; /* Finally we define a caption window to take up all the remaining space. */ captionWindow: CustomBannerWindow bannerArgs = [centreWindow, BannerAfter, rightWindow, BannerTypeText, BannerAlignRight, 100 , BannerSizePercent, BannerStyleBorder] ; /* Modify the Room class to display a picture of itself when the plyaer character enters it. */ modify Room /* The Picture object associated with this room. */ roomPic = nil /* Call our showPic() method when the player character enters this room. */ travelerEntering (traveler, origin) { if(traveler == gPlayerChar) showPic(); } /* Display our picture. */ showPic() { /* If we don't have, deactivate our custom banners, which won't be needed for this room. */ if(roomPic == nil) { captionWindow.deactivate(); picWindow.deactivate(); leftWindow.deactivate(); rightWindow.deactivate(); centreWindow.deactivate(); } else /* Otherwise tell our Picture to display itself. */ roomPic.showPic(); } ; /* Modifications to Thing to have it display a picture of itself when examined. */ modify Thing /* The Picture object associated with this thing. */ pic = nil /* Modify examineStatus to display our picture if we have one. */ examineStatus() { inherited(); if(pic) pic.showPic(); } ; /* Modify the Look action to display a picture of our current location, if it has one. */ modify Look execAction(c) { inherited(c); local loc = gActor.getOutermostRoom(); if(loc.roomPic) loc.showPic(); } ; /* Only use these banners on a terp that can display JPEGs */ modify CustomBannerWindow canDisplay = (systemInfo(SysInfoJpeg)) ; /* From hereon we define Rooms and other objects as normal, defining any associated pictures. */ startRoom: Room 'Radcliffe Square' "From the southeast corner of Radcliffe Square you could go north between the Radcliffe Camera and All Souls College. " /* Define our associated Picture object. */ roomPic: Picture { 'rad_square' // our picture file, rad_square.jpg 'A view from the south-east quadrant of Radcliffe Square' // our caption } north = catteStreet west = notThatWay south = notThatWay ; /* * The player character object. This doesn't have to be called me, but me is a * convenient name. If you change it to something else, rememember to change * gameMain.initialPlayerChar accordingly. */ + me: Player 'you' ; + radcliffeCamera: Fixture 'Radcliffe Camera' "This iconic round building, designed by James Gibbs, is now part of the Bodleian Library, holding among other things the open-shelf undergraduate collections for English Literature and Theology. " /* Our associated Picture object. */ pic: Picture { 'camera' 'A view of the Radcliffe Camera from the south side of Radcliffe Square' } cannotEnterMsg = 'You don\'t have your Bod card with you. ' ; + allSouls: Fixture 'All Souls College' "Founded in 1438, the All Souls traditionally takes no undergraduates. " pic: Picture{ 'all_souls' 'All Souls College seen from Radcliffe Square' } cannotEnterMsg = 'It\'s not your college, and you don\'t have time to go sightseeing right now. ' ; catteStreet: Room 'Catte Street' "Catte Street runs north from Radcliffe Square to the main crossroads. Along the way it passes such famous landmarks as the Sheldonian Theatre and the Bridge of Sighs. " roomPic: Picture { 'catte' 'Looking up Catte Street, with the Bodleian Libary to the left' } south = startRoom north = crossroads ; + bridgeOfSighs: Fixture 'bridge of sighs[n]' "Oxford's so-called <q>Bridge of Sighs</q> joins the two halves of Hertford College separated by New College Lane. " pic: Picture { 'bridge' 'A view of Hertford College\'s <q>Bridge of Sighs</q>, looking down New College Lane from Catte Street. ' } ; + sheldonian: Fixture 'Sheldonian Theatre' "Designed by Sir Christopher Wren, and built in 1664-68, the Sheldonian Theatre is used from a variety of purposes ranging from public concerts to degree ceremonies. " pic: Picture { 'sheldonian' 'A side view of the Sheldonian Theatre, as seen from Catte Street. ' } ; + bod: Fixture 'Bodleian Library; old; bod' "Founded by Sir Thomas Library, the Bodleian Library is the university's central library, although it is one of only several libraries managed by the university. " cannotEnterMsg = 'Right now you\'re more interested in feeding your stomach than your mind. ' pic: Picture { 'library' 'The central quad of the Bodleain Library graced with the presence of an ox. ' } ; crossroads: Room 'Crossroads' "From here Catte Street runs south, Parks Road north, Broad Street west and Holywell Street east; the last of these is your way back to College. On the corner of Holywell Street and Parks road stands the King's Arms. " roomPic: Picture { 'broad' 'The view back down Catte Street from the crossroads. ' } south = catteStreet north = notThatWay west = notThatWay east = holywell ; + ka: Fixture 'King\'s Arms' "A favourite watering-hole for students from your college. " pic: Picture { 'kings_arms' 'The King\'s Arms as seen from the crossroads. ' } cannotEnterMsg = 'You\'d rather eat in College. ' ; holywell: Room 'Holywell Street' "Holywell Street runs eastwards from the crossroads to the junction with Mansfield Road. " west = crossroads east = corner roomPic: Picture { 'holywell' 'The view eastwards from the west end of Holywell Street' } ; corner: Room 'Junction of Holywell Street and Mansfield Road' "At this junction Holywell Street runs on east past New College and west back to the crossroads, while Mansfield Road, your way back to College, runs north. " north = mansfield west = holywell east = notThatWay roomPic: Picture { 'corner' 'The view eastwards down Holywell Street from the junction with Mansfield Road' } ; + Fixture 'New College' "The front of New College visible from Holywell Street is an imposing Gothic structure designed by the Victorian architect Sir George Gilbert Scott. " isProper = true pic: Picture { 'new_college' 'A view of the 19th century front of New College, designed by the Victorian architect George Gilbert Scott' } ; mansfield: Room 'Mansfield Road' "Mansfield College runs north past Harris Manchester College, to the west, and south back into Holywell Street. " north = notThatWay south = corner west = hmc in asExit(west) roomPic: Picture { 'house' 'A house at the southern end of Mansfield Road' } ; + Enterable 'Harris Manchester College;of[prep];entrance hmc' "From the street, it's yet another Victorian Gothic building. " connector = hmc pic: Picture { 'college' 'The Mansfield Road entrance of Harris Manchester College' } ; hmc: Room 'HMC Tate Quad' 'HMC Tate Quad' "The Tate Quad is named from the donor who helped build the college library. From here, the dining hall lies south and the exit from the college east. " south = hall east = mansfield north = notThatWay west = notThatWay roomPic: Picture { 'hmc_quad' 'A view of Harris Manchester College\'s Tate Quad, showing one of the new 1990s buildings (Morrison) to the west, and the Arlosh Hall to the south. ' } ; + Enterable 'Arlosh Hall; dining' "The main college dining hall, built shortly before the First World War, and named for the Arlosh family who provided the money for it. It's also the place where you'll just be in time for lunch, if only you'd hurry up!. " pic: Picture { 'arlosh' 'The rear entrance of the Arlosh Hall' } connector = hall ; hall: Room 'Arlosh Hall' "Here's where you can eat. All you need to do is sit at the table where several of your colleagues are already busily eating. " roomPic: Picture { 'hall' 'Looking down the Arlosh Hall from High Table' } out = hmc north asExit(out) roomBeforeAction() { if(gActionIs(Sit)) { finishGameMsg('You have arrived in time for dinner. ', [finishOptionUndo]); } } ; + Surface, Fixture 'table' "The table is set for dinner, and ready for you to sit. " pic: Picture { 'table' 'The table that awaits you. ' } ; + Actor 'female colleague;;woman;her' "It looks like she's already enjoying her food. " pic: Picture { 'susan' 'A female colleague already enjoying her meal. ' } ; ++ ActorState noResponse = "It would be more seemly to sit down before engaging her in conversation. " specialDesc = "A female colleague you particularly want to talk with is sitting next to a vacant seat. " isInitState = true ; notThatWay: TravelConnector canTravelerPass(traveler) { return nil; } explainTravelBarrier(traveler) { "You could go that way, but you're in hurry to get back to College in time for dinner. "; } ; /* * We define a minimal about menu just to demonstrate and test that we can display a menu and then * have our custom banner window layout automatically restored. */ aboutMenu: MenuItem 'About'; + MenuLongTopicItem 'About the Game' "This is a test and demo of using the CustomBannerWindow class with adv3Lite. It's not much of a game --- all you can do is walk from Radcliffe Square to the Arlosh Hall in Harris Manchester College and sit down for diner, but you get to see pictures of most of the locations along the way and examining the various landmarks mentioned in room descriptions will also cause pictures of them to be displayed. " ; + MenuLongTopicItem 'About the Pictures' 'For this demo I simply used photos I had to hand. Most of them were taken with a cheap digital camera and the picture quality isn\'t great. The quality of the photos isn\'t a reflection of how well either TADS 3 or the CustomBannerWindow extension displays pictures. ' ;