Logo Pending


Unusual arrow keys and cursor behavior

Most players of SCI games would naturally gravitate to using the mouse if it’s an SCI1 game or later. With SCI0 using a text parser and no pathfinding it strikes me personally as more efficient to just use the arrow keys in those games. But that’s just me. The important part here is that some SCI11 games don’t respond to the arrow keys the same way as others do.

SCI0 being effectively stateless in a way, pressing the arrow keys will cause your character to start walking in that direction. Clicking somewhere causes them to walk there in a straight line until something gets in the way. In SCI1 the icon bar introduced a kind of state in that you have several active cursors. In all but a few adventure games, pressing the arrow keys will cause your character to move if the current cursor is Walk. Otherwise, the cursor itself moves around. So if the current cursor is, let’s say, Look, the little eye moves instead of the main character. Unless of course you’d already set them on their way. And of course (unless you’re playing the original KQ5) clicking somewhere with the Walk cursor makes your character find their way around obstacles towards that point.

Two exceptions to this rule that I know of are Space Quest 5 and King’s Quest 6. It turns out if you press the arrow keys with the Walk cursor out, the cursor itself moves. Why is that?

Obviously, there’s something in the game scripts that causes both the usual and specific behavior. Let’s look at both.

The usual behavior is enabled by the Ego class and its handleEvent method. Called from User, this method checks for direction and walk events. If it’s the former, an arrow key was pressed or a joystick was nudged. If it’s the latter, we clicked the Walk cursor somewhere. How do we know it’s Walk? Because User only calls this method if it was. Any other cursor, it runs further down and ends up calling another script that handles moving the cursor around if you press an arrow key.

(method (handleEvent pEvent &tmp eType dir)
  (= eType (pEvent type?))
  (cond
    ; First, see if we have a script that wants the event.
    ((and script (script handleEvent: pEvent)) true)
 
    ; See if it's a direction event
    ((& eType evJOYSTICK)
      (= dir (pEvent message?))
 
      ; don't claim dirStop/CENTER
      (if (and (== dir CENTER) (& eType evKEYBOARD))
        (return (pEvent claimed?))
      )
 
      ; Pressing the same key that started a motion again should stop
      (if (and (& eType evKEYBOARD) ; it's a key
               (== dir (gUser prevDir?)) ; same dir as before
               (IsObject mover)) ; ego is moving
        (= dir CENTER)
      )
 
      ; In the case of a keyDown event, keep the previous direction,
      ; so we know what key stops ego.
      (gUser prevDir: dir)
 
      ; Actor::setDirection sets ego off to walk.
      (self setDirection: dir)
      (pEvent claimed: true)
    )
 
    ((& eType evVERB)
      (if (& eType evMOVE)
        (switch gEgoUseObstacles
          (0 (self setMotion: MoveTo (pEvent x?) (+ (pEvent y?) z)))
          (1 (self setMotion: PolyPath (pEvent x?) (+ (pEvent y?) z)))
          (2 (self setMotion: PolyPath (pEvent x?) (+ (pEvent y?) z) null false))
        )
        (gUser prevDir: 0)
        (pEvent claimed: true)
      else
        (super handleEvent: pEvent)
      )
    )
    (else (super handleEvent: pEvent))
  )
  (return (pEvent claimed?))
)

You might notice the bit about gEgoUseObstacles.  Setting that to zero effectively makes clicks act like in SCI0. At least to the player — the script code is somewhat different.

Now that just leaves the question of why these two games don’t react like that. And it’s really very simple. KQ6 and SQ5 do it each in their own slightly different way, but the basic trick is the same. Because KQ6 seems a bit neater to me, I’ll show that one first:

(instance ego of Ego
  ; Irrelevancies elided
 
  (method (handleEvent pEvent)
    (return
      (if (& (pEvent type?) evJOYSTICK)
        ; Refuse to handle any directional inputs
        (return false)
      else
        ; Let the original Ego class figure it out
        (super handleEvent: pEvent &rest)
      )
    )
  )
)

Simple, huh? Because this returns false for any direction events, User will always end up percolating the event down to PseudoMouse and as such the Walk cursor is moved by the keyboard or joystick instead of the player character. Now let’s see how this compares to Space Quest 5.

(class SQEgo of Ego
  ; Irrelevancies elided
 
  (method (handleEvent pEvent &tmp eType eMsg)
    (= eType (pEvent type?))
    (= eMsg (pEvent message?)) ; woo wasted effort!
    (return
      (cond 
        ((and script (script handleEvent: pEvent)) true)
        ((& eType evJOYSTICK) (return false))
        (else (super handleEvent: pEvent &rest))
      )
    )
  )
)

Seems pretty clear considering what we already know. So much so I won’t bother documenting it much.

The SCI Companion SCI11 template game is based on SQ5 so by default this too applies to any game you make with it. The obvious way to remove this then is to remove GameEgo::handleEvent. I did, and I haven’t looked back since.

[ , ] Leave a Comment

On SCI Windows – Part 7 – The Dating Pool

It was inevitable, really, that I’d end this series with my own game. The Dating Pool uses a simplified take on BorderWindow, with the ability to change the thickness removed.

could’ve just used a BorderWindow, in fact, with bevelWid and shadowWid both set to 1 and I’d get the exact same result. This exact style was in fact one of the examples I’ve shown back there! But I did not, if only because I use titles sometimes.

(class cdWindow of SysWindow
  (properties
    topBordColor 7
    lftBordColor 6
    rgtBordColor 4
    botBordColor 3
    titleBack 3
    titleColor 255
  )
 
  (method (open &tmp oldPort screens)
    ; The established setup
    (= screens VISUAL)
    (if (!= priority -1) (|= screens PRIORITY))
 
    ; Make room for the edges
    (= lsTop (- top 2))
    (if title (-= lsTop 9))
    (= lsLeft (- left 2))
    (= lsRight (+ right 3))
    (= lsBottom (+ bottom 3))
 
    ; Set our custom style
    (= type 128)
 
    ; Always top priority
    (= priority 15)
    (super open:)
 
    ; Now we draw, on the whole screen.
    (= oldPort (GetPort))
    (SetPort 0)
 
    ; Draw our fill...
    (Graph grFILL_BOX top left bottom right screens back 15)
 
    ; Custom title bar support!
    (if title
      (-= top 9)
      (+= bottom 1)
      (Graph grFILL_BOX top left (+ top 9) right screens botBordColor titleBack)
      (Display title dsCOORD left top dsWIDTH (- right left) dsCOLOR titleColor dsFONT 999 dsMODE alCENTER)
    )
 
    ; Draw the frame
    (Graph grDRAW_LINE (- top 1) (- left 1) (- top 1) right topBordColor priority screens)
    (Graph grDRAW_LINE (- top 1) (- left 1) (- bottom 1) (- left 1) lftBordColor priority screens)
    (Graph grDRAW_LINE (- bottom 1) left (- bottom 1) right botBordColor priority screens)
    (Graph grDRAW_LINE top right (- bottom 2) right rgtBordColor priority screens)
 
    ; Draw our drop shadow
    (Graph grDRAW_LINE bottom (+ left 1) bottom right 0 priority screens)
    (Graph grDRAW_LINE top (+ right 1) bottom (+ right 1) 0 priority screens)
 
    ; Show what we have wrought.
    (Graph grUPDATE_BOX (- top 1) (- left 1) (+ bottom 1) (+ right 1) VISUAL)
    (SetPort oldPort)
  )
)

Eagle-eyed viewers might notice that the color properties are set to various grayscales while the image up top is green. This is simply because the instance of this cdWindow class has those colors. The control panel, in contrast, is tan.

And that’s the end of this series. If you have any other SCI games (preferably SCI11 or earlier) that you’d like to see, drop me a line in the comments below and I’ll see what I can do. Barring any of that though…

[ , , ] 2 Comments on On SCI Windows – Part 7 – The Dating Pool

On SCI Windows – Part 6 – Laura Bow, Frontier Pharmacist

It’s a double feature this time because these last two games’ windows are very similar.

These windows have decorations depending on your location or progress. Here are the view resources, both of them #994…

…and here is the code for the lb2Window first:

(instance lb2Win of SysWindow
  (properties
    ; This time, we set the custom bit up here.
    type 128
  )
 
  (method (open &tmp oldPort loop)
    ; Decide which decoration to show depending on our location.
    ; (These have been cut down a bit and the doubles were already
    ;  there. I don't know what's up with that.)
    (cond 
      ((OneOf gRoomNumber 280 210 330 240 260 300) (= loop 0))
      ((OneOf gRoomNumber 210 220 230 260 270 280) (= loop 1))
      ((OneOf gRoomNumber 100 105 110 120 140 150) (= loop 2))
      ((OneOf gRoomNumber 460 660 700 710 715 720) (= loop 4))
      ((OneOf gRoomNumber 335 340 350 355 360 370) (= loop 3))
      (else (= loop 4))
    )
 
    ; Make room for the edges, including the decorations.
    (= lsLeft (- left (/ (CelWide 994 loop 0) 2)))
    ; Yes, LB2 windows adjust for titles, despite the custom bit.
    (= lsTop (- top (if title 19 else 10)))
    (= lsRight (+ right (/ (CelWide 994 loop 0) 2)))
    ; Be at *least* as tall as the decoration.
    (= lsBottom (Max (+ bottom 3) (+ lsTop (CelHigh 994 loop 0) 3)))
 
    ; Always top priority
    (= priority 15)
    (super open:)
 
    ; Now we draw, on the whole screen.
    (= oldPort (GetPort))
    (SetPort 0)
 
    ; Draw our fill...
    (Graph grFILL_BOX top left bottom right 3 gBack 15)
    ; ...border...
    (Graph grDRAW_LINE (- top 1) (- left 1) (- top 1) right gFore 15)
    (Graph grDRAW_LINE (- top 1) (- left 1) bottom (- left 1) gFore 15)
    (Graph grDRAW_LINE bottom (- left 1) bottom right gFore 15)
    (Graph grDRAW_LINE (- top 1) right bottom right gFore 15)
    ; Show what we have wrought...
    (Graph grUPDATE_BOX top left bottom right 1)
    ; ...but also dirty the part where the decorations will go.
    (Graph grUPDATE_BOX lsTop lsLeft (+ lsTop (CelHigh 994 loop 0)) (+ lsLeft (CelWide 994 loop 0)) 1)
    (Graph grUPDATE_BOX lsTop (- lsRight (CelWide 994 loop 0))  (+ lsTop (CelHigh 994 loop 0)) lsRight 1)
    ; Why? I have no idea.
 
    ; Now draw the decorations!
    (DrawCel 994 loop 0 (+ lsLeft 1) (+ lsTop 1) -1)
    (DrawCel 994 loop 1 (- (- lsRight (CelWide 994 loop 0)) 1) (+ lsTop 1) -1)
 
    (SetPort oldPort)
  )
)

Don’t let the thicker right edge fool you — half that line is part of the decoration!

As I said, the code for fpWin is remarkably similar… yet different? Most of it’s just in how the thicker edges are drawn, but a significant bit is in the last part:

(instance fpWin of SysWindow
  (properties
    type 128
  )
 
  (method (open &tmp oldPort loop theLsTop theLsLeft theLsRight)
    ; Decide which decoration to show depending on the act.
    (switch gAct
      (1 (= loop 0))
      (2 (if (Bset 1) (= loop 2) else (= loop 1)))
      (3 (= loop 3))
      (4 (= loop 4))
      (5 (= loop 4))
    )
 
    ; Make room for the edges, including the decorations.
    (= lsLeft (- (- left 3) 15))
    (= lsTop (- (- top 3) (if title 25 else 15)))
    (= lsRight (+ right 3 15))
    (= lsBottom (Max (+ bottom 3) (+ lsTop (CelHigh 994 loop 0) 3)))
 
    ; Always top priority
    (= priority 15)
    (super open:)
 
    ; Now we draw, on the whole screen.
    (= oldPort (GetPort))
    (SetPort 0)
 
    ; Draw our fill...
    (Graph grFILL_BOX top left bottom right 3 gBack 15)
    (if title (= top (- top 10)))
 
    ; Inner box, middling dark...
    (Graph grDRAW_LINE (- top 1) (- left 1) (- top 1) right 17 15)
    (Graph grDRAW_LINE (- top 1) (- left 1) bottom (- left 1) 17 15)
    (Graph grDRAW_LINE bottom (- left 1) bottom right 17 15)
    (Graph grDRAW_LINE (- top 1) right bottom right 17 15)
    ; Middle box, lighter...
    (Graph grDRAW_LINE (- top 2) (- left 2) (- top 2) (+ right 1) 19 15)
    (Graph grDRAW_LINE (- top 2) (- left 2) (+ bottom 1) (- left 2) 19 15)
    (Graph grDRAW_LINE (+ bottom 1) (- left 2) (+ bottom 1) (+ right 1) 19 15)
    (Graph grDRAW_LINE (- top 2) (+ right 1) (+ bottom 1) (+ right 1) 19 15)
    ; And finally the outer, darkest box.
    (Graph grDRAW_LINE (- top 3) (- left 3) (- top 3) (+ right 2) 16 15)
    (Graph grDRAW_LINE (- top 3) (- left 3) (+ bottom 2) (- left 3) 16 15)
    (Graph grDRAW_LINE (+ bottom 2) (- left 3) (+ bottom 2) (+ right 2) 16 15)
    (Graph grDRAW_LINE (- top 3) (+ right 2) (+ bottom 2) (+ right 2) 16 15)
 
    ; Show what we have wrought.
    (Graph grUPDATE_BOX (- top 3) (- left 3) (+ bottom 3) (+ right 3) 1)
 
    ; Unlike LB2, FPFP does this part the hard way.
    (switch gAct
      (1
        (= theLsLeft (+ lsLeft 2))
        (= theLsRight (- (- lsRight 15) 14))
        (= theLsTop lsTop)
      )
      (2
        (if (Bset 1)
          (= theLsLeft lsLeft)
          (= theLsRight (- (- lsRight 15) 13))
          (= theLsTop (+ lsTop 2))
        else
          (= theLsLeft (+ lsLeft 8))
          (= theLsRight (- (- lsRight 15) 14))
          (= theLsTop (+ lsTop 4))
        )
      )
      (3
        (= theLsLeft (+ lsLeft 6))
        (= theLsRight (- (- lsRight 15) 40))
        (= theLsTop (+ lsTop 11))
      )
      (4
        (= theLsLeft (+ lsLeft 7))
        (= theLsRight (- (- lsRight 15) 14))
        (= theLsTop (+ lsTop 8))
      )
      (5
        (= theLsLeft (+ lsLeft 7))
        (= theLsRight (- (- lsRight 15) 14))
        (= theLsTop (+ lsTop 8))
      )
    )
 
    ; Now draw the decorations!
    (DrawCel 994 loop 0 theLsLeft theLsTop -1)
    (DrawCel 994 loop 1 theLsRight theLsTop -1)
 
    (SetPort oldPort)
  )
)

I don’t know why either. Maybe something silly about the decorations?

[ , , , ] Leave a Comment

On SCI Windows – Part 5 – Police Quest 4 but not

Police Quest IV – Open Season is an SCI2 game. As such, it had a radically different way of doing several things. It’s not too hard though to backport one of its elements, namely its translucent windows.

I’ve tweeted about this at length, and actually had to reimplement it all over again. But I did it.

(class TranslucentWindow of SysWindow
  (method (open &tmp oldPort screens scaleX scaleY)
    ; Your standard setup, KQ6 style...
    (= lsLeft (- left 1))
    (= lsRight (+ right 1))
    (= lsTop (- top 1))
    (= lsBottom (+ bottom 1))
 
    (= type 128)
    (= priority 15)
    (super open:)
 
    (= oldPort (GetPort))
    (SetPort 0)
    (= screens VISUAL)
    (if (!= priority -1) (= screens (| screens PRIORITY)))
 
    ; 128 is 100% of the source image size, which is 8x8.
    ; (128 / source) * target --> 16 * target.
    (= scaleX (* (- lsRight lsLeft) 16))
    (= scaleY (* (- lsBottom lsTop) 16))
    (DrawCel 923 1 0 lsLeft lsTop 15 scaleX scaleY)
 
    ; Draw the frame
    (Graph grDRAW_LINE lsTop lsLeft lsTop (- lsRight 1) 0 -1 -1)
    (Graph grDRAW_LINE (- lsBottom 1) lsLeft (- lsBottom 1) (- lsRight 1) 0 -1 -1)
    (Graph grDRAW_LINE lsTop lsLeft (- lsBottom 1) lsLeft 0 -1 -1)
    (Graph grDRAW_LINE lsTop (- lsRight 1) (- lsBottom 1) (- lsRight 1) 0 -1 -1)
 
    ; Show what we have wrought.
    (Graph grUPDATE_BOX (- lsTop 4) (- lsLeft 4) (+ lsBottom 4) (+ lsRight 4) 1)     
    (SetPort oldPort)
  )
)

You’ll also need to do something about DText, up in Interface.sc, or you’ll get ugly opaque text in a translucent window. I use my own extended interpreter so I get the drop shadow for free, but here’s the deal:

(class DText of DItem
  ; Rest of the class elided.
  (method (draw)
    ; We don't get to use hotspot rects anymore, sorry.
    (Display text dsFONT font dsCOORD nsTop nsLeft dsWIDTH (- nsRight nsLeft) dsCOLOR 7 dsBACKGROUND -1 dsALIGN mode)
  )
)

And yeah, the color’s pretty much fixed too — the DrawControl kernel call uses the current port’s colors, which would be the window’s. No such luck unless you significantly mess with things to not only add a color property to DText but also set it somehow while not actually invoking DText yourself so yeeeeaaah this might not be the window style for you.

[ , , ] Leave a Comment

On SCI Windows – Part NaN – The Colonel’s Bequest

I was going to cover Laura Bow – The Colonel’s Bequest but it turns out its custom window is almost exactly the same as the one in KQ5 and it’d be faster to just go over the differences.

  1. No drop shadow.
  2. No color properties.
  3. The corners are in two loops of two cels each.
  4. No memorizing the adjusted coordinates — the dispose method does the adjustments itself.

That’s literally all there is to it.

[ , , ] 2 Comments on On SCI Windows – Part NaN – The Colonel’s Bequest

A Short Tangent – KQ6 Drop Caps

How does King’s Quest VI do that thing where if the Narrator says something, it includes a fancy drop cap? Well, it’s sorta similar to how KQ5 did, but implemented better. Mostly because it’s a newer game and not basically the first on a new engine.

Requirements: a view full of letters, all the same size, and a font where the tab character is as wide as one of these letters.

First, some technical backstory. Narrator is a special class with methods to speak a specific message. It will decide on its own if it should display the passed message in a window of some kind, or play it back as spoken audio. It will also handle timing, disposing of the window and everything at the end. The messages themselves are provided by the Messager class.

A Talker is a kind of Narrator that builds on the above to add talking heads. Fortunately we won’t have to go any deeper into those because this post is all about KQ6’s drop caps, and it’s only Narrator who uses those.

Specifically, KQ6 uses a special DropCapNarrator, in fact. It’s just like a regular Narrator, but overrides the display method to add some extra bits:

(class DropCapNarrator of Narrator
  (properties
    ; Narrator properties elided.
    strView 945 ; the view with the letters in
  )
 
  (method (display theText &tmp theTalkWidth newWindow theFirst theDropCap)
    ; Ensure we don't clip the screen edge.
    (if (> (+ x talkWidth) 318)
      (= theTalkWidth (- 318 x))
    else
      (= theTalkWidth talkWidth)
    )
 
    ; Clone up a new copy of one of those fancy woody windows.
    ((= newWindow (gWindow new:))
      color: color
      back: back
    )
    ; That is, set newWindow's colors to our own.
 
    ; If we have mouse support and we're not using the invisible
    ; cursor, remember what we -do- use and go invisible.
    (if (and (not (HaveMouse)) (!= gCursorNumber 996))
      (= saveCursor gCursorNumber)
      (gGame setCursor: 996)
    else
      (= saveCursor null)
    )
 
    ; Now we're ready for the magic.
 
    ; Grab the first letter of the message text we're to show.
    (= theFirst (StrAt theText 0))
    (if (and (>= 90 theFirst) (>= theFirst 65))
      ; The first character is between 'A' and 'Z' inclusive.
      ; Replace that first character with a tab.
      (StrAt theText 0 9)
      ; Remember, the tab is as wide as a drop cap!
 
      ; Create a new DIcon for our cap and set it up.
      ((= theDropCap (DIcon new:))
        view: strView
        ; Loop 0 is A to M, loop 1 is N to Z.
        loop: (+ 0 (/ (- theFirst 65) 13))
        cel: (mod (- theFirst 65) 13)
      )
 
      ; Now display our message on screen...
      (Print
        window: newWindow
        posn: x y
        font: font
        width: theTalkWidth
        title: (if showTitle name else null)
        ; ...but with the text shifted down a little...
        addText: theText 0 7
        ; ...and that DIcon on top of it.
        addIcon: newDIcon 0 0 0 0
        modeless: true
        init:
      )
    else
      ; Not a valid drop cap so we can skip all that.
      (Print
        window: newWindow
        posn: x y
        font: font
        width: theTalkWidth
        title: (if showTitle name else null)
        ; See? No positioning or DIcon here.
        addText: theText
        modeless: true
        init:
      )
    )
    ; The rest is up to the regular Narrator. 
  )  
)

Personally, I’d set up most of those properties, then stop and consider if we have a drop cap. Duplicating all those sends… it’s still better than what KQ5 did.

[ , , ] 2 Comments on A Short Tangent – KQ6 Drop Caps

On SCI Windows – Part 4 – King’s Quest 6

King’s Quest VI is particularly interesting in that the window frame looks like it has eight parts — four edges, four corners — but the actual view only has the corners. Compared to the simple lines of KQ5, those thick wooden beams have to be views, right?

 But as you can see, they’re not in there.

So how is that woody frame drawn then? The answer: they totally are lines.

(class Kq6Window of SysWindow
  (properties
    colorOne 32
    colorFive 18
    tlColorTwo 17
    tlColorThree 18
    tlColorFour 17
    brColorTwo 18
    brColorThree 17
    brColorFour 16
  )

We interrupt this programming to give a bit of visual aid. Here’s those color properties again, but with the actual colors next to them:

Maybe you can see how they fit together, from the preview on top of the post and the property names.

(method (open &tmp oldPort)
    ; The established setup
    (= screens VISUAL)
    (if (!= priority -1) (= screens (| screens PRIORITY)))
 
    ; Make room for the edges
    (= lsTop (- top 5))
    (= lsLeft (- left 5))
    (= lsRight (+ right 6))
    (= lsBottom (+ bottom 6))
 
    ; Set our custom style
    (= type 128)
 
    ; Always top priority
    (= priority 15)
    (super open:)
 
    ; Now we draw, on the whole screen.
    (= oldPort (GetPort))
    (SetPort 0)
 
    ; Call the method shown below.
    (self drawEdgedWindow: screens)
 
    ; Add the corner pieces
    (DrawCel 930 0 0 (- left 5) (- top 5) -1)
    (DrawCel 930 0 1 (- left 5) (- bottom 1) -1)
    (DrawCel 930 0 2 (- right 1) (- top 5) -1)
    (DrawCel 930 0 3 (- right 1) (- bottom 1) -1)
 
    ; Show what we have wrought.
    (Graph grUPDATE_BOX lsTop lsLeft lsBottom lsRight 1)
    (SetPort oldPort)
  )
 
  ; This is where the actual magic happens.
  (method (drawEdgedWindow screens &tmp line color)
    ; Fill in the window
    (Graph grFILL_BOX top left (+ bottom 1) (+ right 1) screens back priority)
 
    ; Draw the top and left edges first
    (for ((= line 1)) (< line 6) ((++ line))
      (= color
        (switch line
          (1 colorOne) ; inside
          (2 tlColorTwo) ; dark
          (3 tlColorThree) ; light
          (4 tlColorFour) ; dark
          (5 colorFive) ; light
        )
      )
      ; Top edge
      (Graph grDRAW_LINE (- top line) (- left line) (- top line) (+ right line) color priority -1)
      ; Left edge
      (Graph grDRAW_LINE (- top line) (- left line) (+ bottom line) (- left line) color priority -1)
    )
 
    ; Draw the bottom and right edges
    (for ((= line 1)) (< line 6) ((++ line))
      (= color
        (switch line
          (1 colorOne) ; inside
          (2 brColorTwo) ; light
          (3 brColorThree) ; dark
          (4 brColorFour) ; very dark
          (5 colorFive) ; light
        )
      )
      ; Bottom edge
      (Graph grDRAW_LINE (+ bottom line) (- left line) (+ bottom line) (+ right line) color priority -1)
      ; Right edge
      (Graph grDRAW_LINE (- top line) (+ right line) (+ bottom line) (+ right line) color priority -1)
    )
  )
)

Those switch blocks could’ve been left out if they used, say, an array with the color values, but this way they can be exposed as properties. You win some, you lose some.

The drop cap is another story altogether.

[ , , ] Leave a Comment

On SCI Windows – Part 3 – King’s Quest 5

Compared to a BorderWindow, the custom frame used in King’s Quest 5 looks pretty straightforward. Indeed, it’s a few lines and some decorative corner pieces, mostly.

 Here they are right now.

Normally, the color properties would be set on startup depending on the detected color depth. Let’s pretend otherwise. Other than that, the below code is effectively unchanged — all I did was add comments.

(class myWindow of SysWindow
  (properties
    ; SysWindow properties elided.
    back 23
    color 8
    lineColor 19
  )
 
  (method (open &tmp screens theTop theLeft theBottom theRight celHigh celWide)
    ; Determine the size of our corner pieces.
    (= celHigh (CelHigh 944 0 0))
    (= celWide (CelWide 944 0 0))
 
    ; Draw on the main screen.
    (SetPort 0)
 
    ; Make some room.
    (= theTop (- top 8))
    (= theLeft (- left 8))
    (= theBottom (+ bottom 8))
    (= theRight (+ right 8))
 
    (= screens VISUAL)
    (if (!= priority -1) (= screens (| screens PRIORITY)))
 
    ; Save what's underneath us.
    (= underBits (Graph grSAVE_BOX theTop theLeft (+ theBottom 2) (+ theRight 2) screens))
 
    ; Draw a drop shadow
    (Graph grFILL_BOX (+ theTop 2) (+ theLeft 2) (+ theBottom 2) (+ theRight 2) screens 0 priority)
 
    ; Draw our fill
    (Graph grFILL_BOX theTop theLeft theBottom theRight screens back priority)
 
    ; Draw the corner pieces
    (DrawCel 944 0 0 theLeft theTop -1)
    (DrawCel 944 0 1 theLeft (- theBottom celHigh) -1)
    (DrawCel 944 0 2 (- theRight celHigh) theTop -1)
    (DrawCel 944 0 3 (- theRight celHigh) (- theBottom celHigh) -1)
 
    ; Top edges...
    (Graph grDRAW_LINE theTop (+ theLeft celWide) theTop (- theRight celWide) lineColor -1 -1)
    (Graph grDRAW_LINE (+ theTop 2) (+ theLeft celWide) (+ theTop 2) (- theRight celWide) lineColor -1 -1)
    ; Bottom...
    (Graph grDRAW_LINE (- theBottom 1) (+ theLeft celWide) (- theBottom 1) (- theRight celWide) lineColor -1 -1)
    (Graph grDRAW_LINE (- theBottom 3) (+ theLeft celWide) (- theBottom 3) (- theRight celWide) lineColor -1 -1)
    ; Left...
    (Graph grDRAW_LINE (+ theTop celHigh) theLeft (- theBottom celHigh) theLeft lineColor -1 -1 )
    (Graph grDRAW_LINE (+ theTop celHigh) (+ theLeft 2) (- theBottom celHigh) (+ theLeft 2) lineColor -1 -1)
    ; and right.
    (Graph grDRAW_LINE (+ theTop celHigh) (- theRight 1) (- theBottom celHigh) (- theRight 1) lineColor -1 -1)
    (Graph grDRAW_LINE (+ theTop celHigh) (- theRight 3) (- theBottom celHigh) (- theRight 3) lineColor -1 -1)
 
    ; Show what we have wrought.
    (Graph grUPDATE_BOX theTop theLeft (+ theBottom 2) (+ theRight 2) 1)
 
    ; Only now do we change our window type to custom and open it.
    (= type 129)
    (super open:)
 
    (= top theTop)
    (= left theLeft)
    (= bottom (+ theBottom 2))
    (= right (+ theRight 2))
  )
)

Don’t let the math discourage you. Because the corner pieces are drawn before the edge lines, the latter have to be carefully drawn right up against the former. You’ll see in the next part how you can get away with doing the edges first and then drawing the corners.

[ , , ] Leave a Comment

On SCI Windows – Part 2 – BorderWindow

A popular style of window seen in SCI1 games like Police Quest and Space Quest is the BorderWindow. By default it’s thick and gray, but it can really have any thickness and set of five colors you want.

The default looks something like this. Your exact colors may vary as I use a custom palette instead of the standard SCI1 palette, and Space Quest 5 has somewhat tinted grays.
If one were to edit the BorderWindow script a little bit to enable title bars, and did it in a naive way, they’d get something like this. So let’s not do that.
The bevelWid and shadowWid properties can of course be changed on a per-window basis…
…as can the back and four edge colors, topBordColor et al.

But how does this work exactly? Let’s take the actual BorderWindow script code and walk through it.

(class BorderWindow of SysWindow
  (properties
    ; All other properties inherited from SysWindow
    back 5
    topBordColor 7
    lftBordColor 6
    rgtBordColor 4
    botBordColor 3
    bevelWid 3
    shadowWid 2
  )
 
  (method (dispose)
    (super dispose:)
    (SetPort 0)
  )
 
  (method (show)
    (Graph grUPDATE_BOX top left bottom right VISUAL)
  )
 
  (method (open &tmp oldPort screens)
    (SetPort 0) ; Use the entire screen.
 
    ; If we have a priority set, render to that screen too.
    (= screens VISUAL)
    (if (!= priority -1) (= screens (| screens PRIORITY)))
 
    ; Make some room in the "last seen" rect to fit the border.
    (= lsTop (- top bevelWid))
    (= lsLeft (- left bevelWid))
    (= lsRight (+ right bevelWid shadowWid))
    (= lsBottom (+ bottom bevelWid shadowWid))
 
    (= type 128) ; We are custom, as described in the
    ; previous post.
 
    (super open:) ; Let a SysWindow open itself.
    ; This actually just calls the NewWindow kernel call
    ; and saves the returned handle to our window property.
 
    (drawWindow top left bottom right back
      topBordColor lftBordColor botBordColor rgtBordColor
      bevelWid shadowWid
      priority screens
    )
 
    (= oldPort (GetPort)) ; Remember our current port.
    (SetPort 0) ; Use the whole screen again.
    ; Show what we have wrought.
    (Graph grUPDATE_BOX lsTop lsLeft lsBottom lsRight VISUAL)
    (SetPort oldPort)
  )
)

That’s not so bad. The meat of the dish is in drawWindow, obviously:

(procedure (drawWindow t l b r theColor topColor leftColor bottomColor rightColor theBevelWid theShadowWid thePri theMaps &tmp savePort i)
  ; Again, we remember the current port and use the whole screen.
  (= savePort (GetPort))
  (SetPort 0)
 
  ; Fill in the window background.
  (Graph grFILL_BOX t l (+ b 1) (+ r 1) theMaps theColor thePri)
 
  ; Extend our rect to include the bevel.
  (-= t theBevelWid)
  (-= l theBevelWid)
  (+= r theBevelWid)
  (+= b theBevelWid)
 
  ; Draw the top and bottom bevels as simple boxes.
  (Graph grFILL_BOX
    t l (+ t theBevelWid) r
    theMaps topColor thePri
  )
  (Graph grFILL_BOX
    (- b theBevelWid) l b r
    theMaps bottomColor thePri
  )
 
  ; Draw the left and right bevels line by line.
  (for ((= i 0)) (< i theBevelWid) ((++ i))
    (Graph grDRAW_LINE
      (+ t i) (+ l i) (- b (+ i 1)) (+ l i)
      leftColor thePri -1
    )
    (Graph grDRAW_LINE
      (+ t i) (- r (+ i 1)) (- b (+ i 1)) (- r (+ i 1))
      rightColor thePri -1
    )
  )
 
  ; Draw the shadow last. Unlike SysWindows,
  ; these are done as two boxes.
  (if theShadowWid
    (Graph grFILL_BOX
      (+ t theShadowWid) r
      (+ b theShadowWid) (+ r theShadowWid)
      theMaps 0 thePri
    )
    (Graph grFILL_BOX
      b (+ l theShadowWid)
      (+ b theShadowWid) r
      theMaps 0 thePri
    )
  )
 
  (SetPort savePort)
)

To visualize, a BorderWindow (sans shadow) renders like this:

Next time, we take a step back and look at King’s Quest.

[ , ] 1 Comment on On SCI Windows – Part 2 – BorderWindow

AGI versus SCI – A Comparison

AGI, which Sierra used up until around the release of King’s Quest IV – The Perils of Rosella, was a strictly linear scripting language with a fairly simple bytecode. Much simpler than SCI’s object-oriented virtual machine. But how do they compare?

Inspired by this one particular book that’s ostensibly about SCI but contains only AGI snippets from what I’ve seen, here’s the first playable room in King’s Quest IV, and how it’s initialized. I’ve left out a bunch of stuff for clarity. Because it’s so simple, AGI bytecode is pretty easy to decompile:

if (justEntered)
{
  set.horizon(84);
 
  if (!nightTime) { v152 = 1; }
  else { v152 = 101; }
  load.pic(v152);
  draw.pic(v152);
  discard.pic(v152);
 
  //Place Ego according to the previous room
  if (lastRoom == 1) { position(ego, 141, 82); }
  if (lastRoom == 2) { position(ego, 107, 82); }
  if (lastRoom == 9) { position(ego, 96, 82); }
  if (lastRoom == 10) { position(ego, 80, 82); }
  if ((lastRoom == 11 || lastRoom == 15)) { position(ego, 70, 82); }
  if ((lastRoom == 12 || lastRoom == 14)) { position(ego, 60, 82); }
 
  //Add some waves in the water.
  animate.obj(o3);
  load.view(55);
  set.view(o3, 55);
  set.loop(o3, 4);
  set.priority(o3, 5);
  ignore.objs(o3);
  cycle.time(o3, 3);
  position(o3, 64, 152);
  draw(o3);
 
  draw(ego);
  show.pic();
}

Hmm. Compare that to its SCI equivalent. SCI bytecode is much harder to decompile. You can disassemble it, but until SCI Companion came out it wasn’t possible to decompile it. Still here we are:

(instance Room1 of Room
  (properties
    picture 1
    north 25
    south 7
    west 31
    east 2
    horizon 100
  )
 
  (method (init)
    (if gNightTime (= picture 101))
    (super init:)
    (self setRegions: 503 501 504 506)
    (wave1
      isExtra: 1
      view: 665
      loop: 0
      cel: 0
      posn: 203 76
      setPri: 0
      ignoreActors:
      cycleSpeed: 3
      init:
    )
    ; Other waves left out for clarity
    (waves add: wave1 wave2 wave3)
    (wave1 setScript: waveActions)
 
    ; This part is simplified significantly.
    (switch gPreviousRoom
      (south (gEgo posn: 225 188))
      (north (gEgo x: 225 y: (+ horizon 1)))
      (0 (gEgo x: 220 y: 135))
      (east (gEgo x: 318)) ; Y stays the same.
    )
    (gEgo init:)
  )
)

You might notice that there’s no equivalent to draw.pic and discard.pic and such. The Room class handles that by itself the moment Room1 calls (super init:).

[ , , , ] Leave a Comment