@@ -0,0 +1,64 @@
|
||||
package ui
|
||||
|
||||
// screenNav — walking the deck with the arrow keys.
|
||||
//
|
||||
// The screens are connected — each one records where you can get to from it
|
||||
// (content/screen/exit.go) — but walking the map is the player's job, and a
|
||||
// screen whose beat is not written yet has nothing in it to walk towards. LEFT
|
||||
// and RIGHT step to the previous and next screen so the deck can be reviewed as
|
||||
// a deck: in concept-art order, wrapping at both ends. It is a tool for looking
|
||||
// at the game, not a way through it.
|
||||
//
|
||||
// It is deliberately inert whenever the game is actually doing something: no
|
||||
// stepping out of a cutscene, a menu, or a stopped world during dialogue. So
|
||||
// once a beat owns a screen, this cannot cut across it.
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/world"
|
||||
)
|
||||
|
||||
type screenNav struct {
|
||||
Name string
|
||||
W *world.World
|
||||
}
|
||||
|
||||
func (n *screenNav) GetName() string { return n.Name }
|
||||
func (n *screenNav) Draw(dst *ebiten.Image, ctx *inkwell.UICtx) {}
|
||||
|
||||
func (n *screenNav) Tick(ctx *inkwell.UICtx) {
|
||||
if !n.W.HUDVisible() || n.W.Paused() {
|
||||
return
|
||||
}
|
||||
step := 0
|
||||
// inkwell's Input only covers the mouse; read the keys directly.
|
||||
switch {
|
||||
case inpututil.IsKeyJustPressed(ebiten.KeyArrowRight):
|
||||
step = 1
|
||||
case inpututil.IsKeyJustPressed(ebiten.KeyArrowLeft):
|
||||
step = -1
|
||||
default:
|
||||
return
|
||||
}
|
||||
if next := n.neighbour(ctx.Game, step); next != "" {
|
||||
n.W.Do(inkwell.GoTo(next))
|
||||
}
|
||||
}
|
||||
|
||||
// neighbour returns the screen step places from the current one, wrapping.
|
||||
// Registration order is the deck order — see content/screen/screen.go.
|
||||
func (n *screenNav) neighbour(g *inkwell.Game, step int) string {
|
||||
deck := g.SceneManager.Names()
|
||||
if len(deck) < 2 {
|
||||
return ""
|
||||
}
|
||||
for i, name := range deck {
|
||||
if name == n.W.Scene() {
|
||||
return deck[((i+step)%len(deck)+len(deck))%len(deck)]
|
||||
}
|
||||
}
|
||||
return deck[0]
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
package ui
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
|
||||
"git.teletypegames.org/games/realworld/internal/world"
|
||||
)
|
||||
|
||||
// The arrow keys walk the deck in registration order and wrap at both ends —
|
||||
// until the beats grow doors, that walk is the only way through most screens,
|
||||
// so a dead end at either end would strand the player on a greybox screen.
|
||||
func TestScreenNavWrapsBothWays(t *testing.T) {
|
||||
g := inkwell.NewGame("t", ScreenW, ScreenH)
|
||||
for _, n := range []string{"a", "b", "c"} {
|
||||
g.SceneManager.Register(inkwell.Scene{Name: n, Background: "bg"})
|
||||
}
|
||||
w := world.New(g)
|
||||
nav := &screenNav{Name: "screen_nav", W: w}
|
||||
at := func(name string) { world.EnterScene(w, name).Start().Tick(&inkwell.Ctx{Game: g}) }
|
||||
|
||||
for _, tc := range []struct {
|
||||
from string
|
||||
step int
|
||||
want string
|
||||
}{
|
||||
{"a", 1, "b"}, {"b", 1, "c"}, {"c", 1, "a"},
|
||||
{"c", -1, "b"}, {"b", -1, "a"}, {"a", -1, "c"},
|
||||
} {
|
||||
at(tc.from)
|
||||
if got := nav.neighbour(g, tc.step); got != tc.want {
|
||||
t.Errorf("from %q step %+d = %q, want %q", tc.from, tc.step, got, tc.want)
|
||||
}
|
||||
}
|
||||
}
|
||||
+37
-21
@@ -7,26 +7,31 @@ package ui
|
||||
// 0 │ ALLEY — paused SRP: 1 tape │ TopBar (20)
|
||||
// 20 ├────────────────────────────────────────────────────────────────────┤
|
||||
// │ │
|
||||
// │ scene — hotspots, characters, SpeechBubble │ Scene (244)
|
||||
// │ scene — hotspots, characters, SpeechBubble │ Scene (242)
|
||||
// │ │
|
||||
// 264├─────────────────────────────────────────┬──────────────────────────┤
|
||||
// 262├─────────────────────────────────────────┬──────────────────────────┤
|
||||
// │ Use hook on: floor grate │ DEX │
|
||||
// │ ┌───┬───┬───┬───┐ ┌───────┐┌────────┐ │ "The hook is a hand │ HUD (135)
|
||||
// │ ┌───┬───┬───┬───┐ ┌───────┐┌────────┐ │ "The hook is a hand │ HUD (118)
|
||||
// │ ├───┼───┼───┼───┤ │ 1 DEX ││ 2 — │ │ shorter than the gap." │
|
||||
// │ └───┴───┴───┴───┘ └───────┘└────────┘ │ │
|
||||
// 399└─────────────────────────────────────────┴──────────────────────────┘
|
||||
// 379└─────────────────────────────────────────┴──────────────────────────┘
|
||||
// InventoryBar TapeSlots TapeChannel
|
||||
//
|
||||
// On the resolution: the wiki's art brief asks for a "320×200 VGA look", and
|
||||
// inkwell's widget defaults are authored for exactly that. We render at 640×400
|
||||
// — the same 16:10, exactly 2× the brief, so art drawn at 320×200 upscales
|
||||
// cleanly — because the engine's text is a fixed 6×16 debug font. At 320×200 a
|
||||
// 16px glyph is 8% of the screen height and nothing fits; at 640×400 it lands
|
||||
// at 4%, which is the proportion the design was drawn for.
|
||||
// On the resolution: the screen is exactly one painted background. inkwell
|
||||
// stretches a scene background over the WHOLE screen (core.engine.go), not over
|
||||
// the region left free by the HUD, so any screen size other than the art's own
|
||||
// squashes every painting — and the deck is drawn at 640×380
|
||||
// (games/realworld_screen_assets). Hence 640×380: the art lands pixel for
|
||||
// pixel, and the HUD is an opaque strip laid over its foot.
|
||||
//
|
||||
// Everything vertical is derived from LineH below rather than from the
|
||||
// wireframe deck's ratios, so the layout cannot drift out of step with the font
|
||||
// again.
|
||||
// That still satisfies what the wiki's "320×200 VGA look" brief was after — a
|
||||
// low, wide VGA frame at an exact 2× of a 320-wide canvas — and it still keeps
|
||||
// the engine's fixed 6×16 debug font readable: a 16px glyph is 4% of 380, the
|
||||
// proportion the design was drawn for. (At 320×190 it would be 8%, rows would
|
||||
// overlap and the top bar could not hold a line.)
|
||||
//
|
||||
// Everything vertical is derived from LineH and from ScreenH below, never from
|
||||
// the wireframe deck's ratios, so the layout cannot drift out of step with the
|
||||
// font — or with the art — again.
|
||||
|
||||
import (
|
||||
inkwell "git.teletypegames.org/engines/inkwell"
|
||||
@@ -39,8 +44,11 @@ import (
|
||||
|
||||
// Screen and layout, in internal (unscaled) pixels.
|
||||
const (
|
||||
// The screen is the size of one painted background, because the engine
|
||||
// scales a background to the full screen: at any other size the art is
|
||||
// squashed. See the note above.
|
||||
ScreenW = 640
|
||||
ScreenH = 400
|
||||
ScreenH = 380
|
||||
|
||||
// LineH is one text row: the glyph cell plus leading. Every text-bearing
|
||||
// box is sized as a multiple of this, so rows can never overlap.
|
||||
@@ -48,14 +56,21 @@ const (
|
||||
Pad = 6 // inner padding for every panel
|
||||
|
||||
TopBarH = LineH + 2 // 20 — one row plus a hairline of breathing room
|
||||
HUDTop = 264 // the scene | HUD divider
|
||||
DividerX = 394 // world | tape channel (61.6% of the width)
|
||||
|
||||
// The HUD strip is as short as its own contents allow, and is measured
|
||||
// from the bottom of the screen — every pixel it takes is a pixel of the
|
||||
// painting it covers. It has to hold the status row and, under it, the two
|
||||
// rows of inventory slots (the tallest thing in the strip), padded above
|
||||
// and below.
|
||||
HUDH = Pad + LineH + 4 + invSlot*2 + invGap + Pad // 118
|
||||
HUDTop = ScreenH - HUDH // 262 — the scene | HUD divider
|
||||
|
||||
// Derived HUD rows.
|
||||
statusY = HUDTop + Pad // 270
|
||||
slotsY = HUDTop + Pad + LineH // 288
|
||||
statusY = HUDTop + Pad // 268
|
||||
slotsY = HUDTop + Pad + LineH // 286
|
||||
slotsH = LineH*2 + Pad*2 // 48 — two rows plus padding
|
||||
invY = slotsY + 4 // 292
|
||||
invY = slotsY + 4 // 290
|
||||
invSlot = 40
|
||||
invGap = 4
|
||||
)
|
||||
@@ -70,6 +85,7 @@ func Register(w *world.World) {
|
||||
// it does, and it only ever catches clicks that land on the scene.
|
||||
g.UIManager.Register(&UseWithGuard{Name: "usewith_guard", W: w})
|
||||
g.UIManager.Register(&world.ActionPump{Name: "pump", W: w})
|
||||
g.UIManager.Register(&screenNav{Name: "screen_nav", W: w})
|
||||
g.UIManager.Register(&windowSizer{Name: "window", Scale: 2})
|
||||
g.UIManager.Register(&inkwell.HotspotDebug{Name: "hotspot_debug"})
|
||||
|
||||
@@ -129,8 +145,8 @@ func Register(w *world.World) {
|
||||
|
||||
// windowSizer resizes the window once, on the first frame.
|
||||
//
|
||||
// inkwell.Run hardcodes a 4× window (core.dsl.go), which at 640×400 would be
|
||||
// 2560×1600 — larger than most laptop screens. Run sets the size before
|
||||
// inkwell.Run hardcodes a 4× window (core.dsl.go), which at 640×380 would be
|
||||
// 2560×1520 — larger than most laptop screens. Run sets the size before
|
||||
// entering the loop, so the only place to override it is the first tick.
|
||||
type windowSizer struct {
|
||||
Name string
|
||||
|
||||
Reference in New Issue
Block a user