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.

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. '
;