Scene exits, Back(), and the current/previous scene accessors
ci/woodpecker/push/woodpecker Pipeline was successful
ci/woodpecker/push/woodpecker Pipeline was successful
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>
This commit is contained in:
+121
@@ -0,0 +1,121 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user