Files
inkwell/scene.exit.go
mr.zeroandClaude Opus 5 f9745e4266
ci/woodpecker/push/woodpecker Pipeline was successful
Scene exits, Back(), and the current/previous scene accessors
A connection between two scenes had to be hand-built as a hotspot: an area,
a cursor, a GoTo, and a flag check written out again for every door. Scene
now carries Exits, and the engine expands each one into a hotspot.

- Exit{To, Label, Side, Area, Needs, Blocked, OnLook, OnTake} in scene.exit.go.
  With no Area an exit is an edge strip, sized as a fraction of Game.SceneArea()
  so a game whose HUD covers the foot of the window gets strips inside the
  painting. Game.ExitLook / Game.ExitTake supply the look and take responses
  once for the whole game, so the phrasing is not repeated per exit.
- Game.SceneHotspots(name) is the scene's own hotspots followed by its exits,
  cached per scene. Authored hotspots come first, so a painted thing beats the
  edge strip wherever the two overlap. HotspotAt and HotspotDebug read it.
- Game.CurrentScene() / PreviousScene(), both persisted in the save file. The
  engine tracked the current scene but exposed no accessor, so every game had
  to shadow it to know where it was.
- Back() returns to the previous scene — what an exit with an empty To binds
  to, for a scene reachable from several rooms whose way out is a direction
  rather than a place.
- Validate rejects an exit naming a scene nobody registered.

Generated hotspots are named ExitName(To) ("exit:street", "exit:back"), so the
location graph can be read straight back out of the registered scenes.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-30 00:09:20 +02:00

122 lines
3.4 KiB
Go

package inkwell
// ExitSide is where on the picture an exit sits when it has no Area of its
// own: off one edge, into the depth of the picture, or out towards the
// camera. Until the doors are measured on the artwork, an edge strip is the
// convention every 1990s point & click used, and re-aiming an exit at a real
// door later is a one-field change.
type ExitSide int
const (
ExitLeft ExitSide = iota // off the left edge
ExitRight // off the right edge
ExitBack // into the depth of the picture
ExitNear // out towards the camera
)
// Exit is one way out of a scene. The engine turns it into a Hotspot, so a
// connection can be declared as data — where it leads, what the status line
// calls it, and what has to be true before it opens — instead of being spelled
// out as another hand-built hotspot.
//
// A connection stays machine-readable: the generated hotspot is named
// ExitName(To), so the location graph can be read straight back out of the
// registered scenes.
type Exit struct {
// To is the scene the exit leads to. Empty means "back the way you came" —
// for a scene reachable from several others, where the way out is a
// direction rather than a place.
To string
// Label is what the status line calls it.
Label string
// Side places the exit when Area is nil.
Side ExitSide
// Area overrides the edge strip Side would give it.
Area Shape
// Needs is a flag that has to be set before the exit opens; Blocked is
// what happens while it is not. The exit is visible either way — a locked
// door still says it is a door.
Needs string
Blocked Action
// OnLook and OnTake override Game.ExitLook and Game.ExitTake for this one
// exit.
OnLook Action
OnTake Action
}
// ExitName is the hotspot name generated for an exit to the named scene.
func ExitName(to string) string {
if to == "" {
return "exit:back"
}
return "exit:" + to
}
func (e Exit) hotspot(g *Game) Hotspot {
var travel Action = GoTo(e.To)
if e.To == "" {
travel = Back()
}
if e.Needs != "" {
if e.Blocked != nil {
travel = If(Flag(e.Needs), travel, e.Blocked)
} else {
travel = If(Flag(e.Needs), travel)
}
}
area := e.Area
if area == nil {
area = e.Side.area(g.SceneArea())
}
look, take := e.OnLook, e.OnTake
if look == nil && g.ExitLook != nil {
look = g.ExitLook(e)
}
if take == nil && g.ExitTake != nil {
take = g.ExitTake(e)
}
return Hotspot{
Name: ExitName(e.To),
Label: e.Label,
Area: area,
Cursor: CursorExit,
OnLook: look,
OnUse: travel,
OnTake: take,
}
}
// The edge strips, as fractions of the scene area, so a game that hands over
// only part of the window to the picture (a HUD along the foot, letterbox
// bars) gets strips that sit inside the picture rather than under the HUD.
const (
exitEdgeWidth = 0.0875
exitEdgeInset = 0.080
exitEdgeHeight = 0.876
exitDoorWidth = 0.275
exitBackInset = 0.033
exitBackHeight = 0.347
exitNearHeight = 0.198
)
func (s ExitSide) area(r Rectangle) Shape {
edge := r.W * exitEdgeWidth
door := r.W * exitDoorWidth
doorX := r.X + (r.W-door)/2
switch s {
case ExitLeft:
return Rect(r.X, r.Y+r.H*exitEdgeInset, edge, r.H*exitEdgeHeight)
case ExitRight:
return Rect(r.X+r.W-edge, r.Y+r.H*exitEdgeInset, edge, r.H*exitEdgeHeight)
case ExitBack:
return Rect(doorX, r.Y+r.H*exitBackInset, door, r.H*exitBackHeight)
default:
return Rect(doorX, r.Y+r.H*(1-exitNearHeight), door, r.H*exitNearHeight)
}
}