2020-03-23 00:33:29 +00:00
|
|
|
package ui
|
|
|
|
|
|
|
|
import (
|
2020-04-14 03:14:49 +01:00
|
|
|
"fmt"
|
2020-03-23 00:33:29 +00:00
|
|
|
"image"
|
2020-03-26 23:35:34 +00:00
|
|
|
"log"
|
2020-03-24 20:21:55 +00:00
|
|
|
|
2020-04-01 19:45:57 +01:00
|
|
|
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
2020-03-24 20:21:55 +00:00
|
|
|
"code.ur.gs/lupine/ordoor/internal/menus"
|
2020-03-23 00:33:29 +00:00
|
|
|
)
|
|
|
|
|
2020-04-01 19:45:57 +01:00
|
|
|
type AlignMode int
|
|
|
|
|
|
|
|
const (
|
|
|
|
AlignModeCentre AlignMode = 0
|
|
|
|
AlignModeLeft AlignMode = 1
|
|
|
|
)
|
|
|
|
|
2020-03-23 00:33:29 +00:00
|
|
|
// A non-interactive element is not a widget; it merely displays some pixels and
|
|
|
|
// may optionally have a tooltip for display within bounds.
|
|
|
|
//
|
|
|
|
// For non-animated non-interactive elements, just give them a single frame.
|
|
|
|
type noninteractive struct {
|
2020-04-14 03:14:49 +01:00
|
|
|
locator string
|
|
|
|
frames animation
|
|
|
|
rect image.Rectangle
|
2020-03-24 20:21:55 +00:00
|
|
|
|
2020-03-26 22:09:26 +00:00
|
|
|
// Some non-interactives, e.g., overlays, are an image + text to be shown
|
2020-04-01 19:45:57 +01:00
|
|
|
label *label
|
2020-03-26 22:09:26 +00:00
|
|
|
|
2020-03-27 02:16:54 +00:00
|
|
|
clickImpl // Alright, alright, it turns out the bridge mission briefing is clickable
|
2020-03-24 20:21:55 +00:00
|
|
|
hoverImpl
|
|
|
|
}
|
|
|
|
|
2020-04-01 19:45:57 +01:00
|
|
|
// Paint some text to screen
|
|
|
|
type label struct {
|
|
|
|
align AlignMode
|
|
|
|
rect image.Rectangle
|
|
|
|
text string
|
|
|
|
font *assetstore.Font
|
|
|
|
}
|
|
|
|
|
2020-03-27 00:54:57 +00:00
|
|
|
// This particular animation has entry and exit sequences, which are invoked
|
|
|
|
// when entering and leaving hover, respectively. Example: bridge doors
|
|
|
|
type animationHover struct {
|
|
|
|
noninteractive // Use the frames in here for the "enter hover" animation
|
|
|
|
exitFrames animation // and here the "exit hover" animation
|
|
|
|
|
|
|
|
atTick int // Tracks progress through the frames
|
|
|
|
opening bool
|
|
|
|
closing bool
|
|
|
|
}
|
|
|
|
|
2020-04-14 03:14:49 +01:00
|
|
|
func (d *Driver) buildNoninteractive(p *menus.Properties) (*noninteractive, error) {
|
|
|
|
// FIXME: SpriteID takes precedence over SHARE if present, but is that
|
|
|
|
// always right?
|
|
|
|
spriteId := p.BaseSpriteID()
|
|
|
|
if spriteId < 0 {
|
|
|
|
return nil, fmt.Errorf("No base sprite for %v", p.Locator)
|
2020-03-24 20:21:55 +00:00
|
|
|
}
|
|
|
|
|
2020-04-14 03:14:49 +01:00
|
|
|
sprite, err := d.menu.Sprite(p.ObjectIdx, spriteId)
|
2020-03-24 20:21:55 +00:00
|
|
|
if err != nil {
|
2020-04-01 01:38:42 +01:00
|
|
|
return nil, err
|
2020-03-24 20:21:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
ni := &noninteractive{
|
2020-04-14 03:14:49 +01:00
|
|
|
locator: p.Locator,
|
|
|
|
frames: animation{sprite.Image},
|
|
|
|
rect: sprite.Rect,
|
2020-03-24 20:21:55 +00:00
|
|
|
}
|
|
|
|
|
2020-04-01 01:38:42 +01:00
|
|
|
return ni, nil
|
2020-03-24 20:21:55 +00:00
|
|
|
}
|
|
|
|
|
2020-04-14 03:14:49 +01:00
|
|
|
func (d *Driver) buildStatic(p *menus.Properties) (*noninteractive, *Widget, error) {
|
|
|
|
ni, err := d.buildNoninteractive(p)
|
2020-03-24 20:21:55 +00:00
|
|
|
if err != nil {
|
2020-04-14 03:14:49 +01:00
|
|
|
return nil, nil, err
|
2020-03-24 20:21:55 +00:00
|
|
|
}
|
|
|
|
|
2020-04-14 03:14:49 +01:00
|
|
|
ni.hoverImpl.text = p.Text
|
|
|
|
|
|
|
|
widget := &Widget{
|
|
|
|
Locator: ni.locator,
|
2020-04-14 15:11:25 +01:00
|
|
|
ownClickables: []clickable{ni}, // FIXME: credits background needs to be clickable
|
2020-04-14 03:14:49 +01:00
|
|
|
ownHoverables: []hoverable{ni},
|
|
|
|
ownPaintables: []paintable{ni},
|
2020-03-24 20:21:55 +00:00
|
|
|
}
|
|
|
|
|
2020-04-14 03:14:49 +01:00
|
|
|
return ni, widget, nil
|
|
|
|
}
|
2020-03-24 20:21:55 +00:00
|
|
|
|
2020-04-14 03:14:49 +01:00
|
|
|
func (d *Driver) buildHypertext(p *menus.Properties) (*noninteractive, *Widget, error) {
|
|
|
|
ni, err := d.buildNoninteractive(p)
|
|
|
|
if err != nil {
|
|
|
|
return nil, nil, err
|
|
|
|
}
|
|
|
|
|
|
|
|
// FIXME: check if this is still needed on the bridge -> briefing transition
|
|
|
|
widget := &Widget{
|
|
|
|
Locator: ni.locator,
|
|
|
|
ownClickables: []clickable{ni},
|
|
|
|
ownHoverables: []hoverable{ni},
|
|
|
|
}
|
|
|
|
|
|
|
|
return ni, widget, nil
|
2020-03-24 20:21:55 +00:00
|
|
|
}
|
|
|
|
|
2020-03-26 22:09:26 +00:00
|
|
|
// An overlay is a static image + some text that needs to be rendered
|
2020-04-14 03:14:49 +01:00
|
|
|
func (d *Driver) buildOverlay(p *menus.Properties) (*noninteractive, *Widget, error) {
|
|
|
|
ni, err := d.buildNoninteractive(p)
|
2020-03-26 22:09:26 +00:00
|
|
|
if err != nil {
|
2020-04-14 03:14:49 +01:00
|
|
|
return nil, nil, err
|
2020-03-26 22:09:26 +00:00
|
|
|
}
|
|
|
|
|
2020-04-14 03:14:49 +01:00
|
|
|
widget := &Widget{
|
|
|
|
Locator: ni.locator,
|
|
|
|
ownPaintables: []paintable{ni},
|
2020-03-26 22:09:26 +00:00
|
|
|
}
|
|
|
|
|
2020-04-14 03:14:49 +01:00
|
|
|
if p.Text != "" {
|
2020-03-30 00:15:19 +01:00
|
|
|
// FIXME: is this always right? Seems to make sense for Main.mnu
|
2020-04-14 03:14:49 +01:00
|
|
|
fnt := d.menu.Font(p.FontType/10 - 1)
|
2020-03-30 00:15:19 +01:00
|
|
|
|
2020-04-01 19:45:57 +01:00
|
|
|
ni.label = &label{
|
|
|
|
font: fnt,
|
|
|
|
rect: ni.rect, // We will be centered by default
|
2020-04-14 03:14:49 +01:00
|
|
|
text: p.Text,
|
2020-03-26 22:09:26 +00:00
|
|
|
}
|
2020-03-26 23:35:34 +00:00
|
|
|
} else {
|
2020-04-14 03:14:49 +01:00
|
|
|
log.Printf("Overlay without text detected in %v", p.Locator)
|
2020-03-26 22:09:26 +00:00
|
|
|
}
|
|
|
|
|
2020-04-14 03:14:49 +01:00
|
|
|
return ni, widget, nil
|
2020-03-26 22:09:26 +00:00
|
|
|
}
|
|
|
|
|
2020-03-24 20:21:55 +00:00
|
|
|
// An animation is a non-interactive element that displays something in a loop
|
2020-04-14 03:14:49 +01:00
|
|
|
func (d *Driver) buildAnimationSample(p *menus.Properties) (*noninteractive, *Widget, error) {
|
|
|
|
sprite, err := d.menu.Sprite(p.ObjectIdx, p.SpriteId[0])
|
2020-03-24 20:21:55 +00:00
|
|
|
if err != nil {
|
2020-04-14 03:14:49 +01:00
|
|
|
return nil, nil, err
|
2020-03-24 20:21:55 +00:00
|
|
|
}
|
|
|
|
|
2020-04-14 03:14:49 +01:00
|
|
|
frames, err := d.menu.Images(p.ObjectIdx, p.SpriteId[0], p.DrawType)
|
2020-03-24 20:21:55 +00:00
|
|
|
if err != nil {
|
2020-04-14 03:14:49 +01:00
|
|
|
return nil, nil, err
|
2020-03-24 20:21:55 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
ani := &noninteractive{
|
2020-04-14 03:14:49 +01:00
|
|
|
locator: p.Locator,
|
2020-03-24 20:21:55 +00:00
|
|
|
frames: animation(frames),
|
2020-04-14 03:14:49 +01:00
|
|
|
hoverImpl: hoverImpl{text: p.Text},
|
2020-03-24 20:21:55 +00:00
|
|
|
rect: sprite.Rect,
|
|
|
|
}
|
|
|
|
|
2020-04-14 03:14:49 +01:00
|
|
|
widget := &Widget{
|
|
|
|
ownHoverables: []hoverable{ani},
|
|
|
|
ownPaintables: []paintable{ani},
|
|
|
|
}
|
2020-03-24 20:21:55 +00:00
|
|
|
|
2020-04-14 03:14:49 +01:00
|
|
|
return ani, widget, nil
|
2020-03-24 20:21:55 +00:00
|
|
|
}
|
|
|
|
|
2020-04-14 03:14:49 +01:00
|
|
|
func (d *Driver) buildAnimationHover(p *menus.Properties) (*animationHover, *Widget, error) {
|
|
|
|
sprite, err := d.menu.Sprite(p.ObjectIdx, p.SpriteId[0])
|
2020-03-27 00:54:57 +00:00
|
|
|
if err != nil {
|
2020-04-14 03:14:49 +01:00
|
|
|
return nil, nil, err
|
2020-03-27 00:54:57 +00:00
|
|
|
}
|
|
|
|
|
2020-04-14 03:14:49 +01:00
|
|
|
enterFrames, err := d.menu.Images(p.ObjectIdx, p.SpriteId[0], p.DrawType)
|
2020-03-27 00:54:57 +00:00
|
|
|
if err != nil {
|
2020-04-14 03:14:49 +01:00
|
|
|
return nil, nil, err
|
2020-03-27 00:54:57 +00:00
|
|
|
}
|
|
|
|
|
2020-04-14 03:14:49 +01:00
|
|
|
exitFrames, err := d.menu.Images(p.ObjectIdx, p.SpriteId[0]+p.DrawType, p.DrawType)
|
2020-03-27 00:54:57 +00:00
|
|
|
if err != nil {
|
2020-04-14 03:14:49 +01:00
|
|
|
return nil, nil, err
|
2020-03-27 00:54:57 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
ani := &animationHover{
|
|
|
|
noninteractive: noninteractive{
|
2020-04-14 03:14:49 +01:00
|
|
|
locator: p.Locator,
|
2020-03-27 00:54:57 +00:00
|
|
|
frames: animation(enterFrames),
|
2020-04-14 03:14:49 +01:00
|
|
|
hoverImpl: hoverImpl{text: p.Text},
|
2020-03-27 00:54:57 +00:00
|
|
|
rect: sprite.Rect,
|
|
|
|
},
|
|
|
|
|
|
|
|
exitFrames: animation(exitFrames),
|
|
|
|
}
|
|
|
|
|
2020-04-14 03:14:49 +01:00
|
|
|
widget := &Widget{
|
|
|
|
ownHoverables: []hoverable{ani},
|
|
|
|
ownPaintables: []paintable{ani},
|
|
|
|
}
|
2020-03-27 00:54:57 +00:00
|
|
|
|
2020-04-14 03:14:49 +01:00
|
|
|
return ani, widget, nil
|
2020-03-27 00:54:57 +00:00
|
|
|
}
|
|
|
|
|
2020-03-27 02:16:54 +00:00
|
|
|
func (n *noninteractive) id() string {
|
2020-04-14 03:14:49 +01:00
|
|
|
return n.locator
|
2020-03-27 02:16:54 +00:00
|
|
|
}
|
|
|
|
|
2020-03-24 20:21:55 +00:00
|
|
|
func (n *noninteractive) bounds() image.Rectangle {
|
|
|
|
return n.rect
|
2020-03-23 00:33:29 +00:00
|
|
|
}
|
|
|
|
|
2020-03-24 20:21:55 +00:00
|
|
|
func (n *noninteractive) regions(tick int) []region {
|
2020-03-26 22:09:26 +00:00
|
|
|
out := oneRegion(n.bounds().Min, n.frames.image(tick))
|
|
|
|
|
2020-04-01 19:45:57 +01:00
|
|
|
// Text for a noninteractive is not registered separately
|
|
|
|
if n.label != nil {
|
|
|
|
out = append(out, n.label.regions(tick)...)
|
2020-03-26 22:09:26 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
return out
|
2020-03-23 00:33:29 +00:00
|
|
|
}
|
2020-03-27 00:54:57 +00:00
|
|
|
|
|
|
|
func (a *animationHover) regions(tick int) []region {
|
|
|
|
if a.opening || a.closing {
|
|
|
|
var anim animation
|
|
|
|
if a.opening {
|
|
|
|
anim = a.frames
|
|
|
|
} else {
|
|
|
|
anim = a.exitFrames
|
|
|
|
}
|
|
|
|
|
|
|
|
out := oneRegion(a.bounds().Min, anim[a.atTick])
|
|
|
|
|
|
|
|
if a.atTick < len(anim)-1 {
|
|
|
|
a.atTick += 1
|
|
|
|
} else if !a.hoverState() {
|
|
|
|
a.closing = false
|
|
|
|
}
|
|
|
|
|
|
|
|
return out
|
|
|
|
}
|
|
|
|
|
|
|
|
// Nothing doing, show a closed door
|
|
|
|
return oneRegion(a.bounds().Min, a.frames.image(0))
|
|
|
|
}
|
|
|
|
|
|
|
|
func (a *animationHover) setHoverState(value bool) {
|
|
|
|
a.atTick = 0
|
|
|
|
a.opening = value
|
|
|
|
a.closing = !value
|
|
|
|
|
|
|
|
a.hoverImpl.setHoverState(value)
|
|
|
|
}
|
2020-04-01 19:45:57 +01:00
|
|
|
|
|
|
|
// Top-left of where to start drawing the text. We want it to appear to be in
|
|
|
|
// the centre of the rect.
|
|
|
|
//
|
|
|
|
// TODO: additional modes (left-aligned, especially)
|
|
|
|
func (l *label) pos() image.Point {
|
|
|
|
pos := l.rect.Min
|
|
|
|
|
|
|
|
textRect := l.font.CalculateBounds(l.text)
|
|
|
|
|
|
|
|
// Centre the text horizontally
|
|
|
|
if l.align == AlignModeCentre {
|
|
|
|
xSlack := l.rect.Dx() - textRect.Dx()
|
|
|
|
if xSlack > 0 {
|
|
|
|
pos.X += xSlack / 2
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// FIXME: we're giving it 8pts of left to not look horrible
|
|
|
|
pos.X += 8
|
|
|
|
}
|
|
|
|
|
|
|
|
// Centre the text vertically
|
|
|
|
ySlack := l.rect.Dy() - textRect.Dy()
|
|
|
|
if ySlack > 0 {
|
|
|
|
pos.Y += ySlack / 2
|
|
|
|
}
|
|
|
|
|
|
|
|
return pos
|
|
|
|
}
|
|
|
|
|
|
|
|
func (l *label) regions(tick int) []region {
|
|
|
|
var out []region
|
|
|
|
|
|
|
|
pt := l.pos()
|
|
|
|
|
|
|
|
for _, r := range l.text {
|
|
|
|
glyph, err := l.font.Glyph(r)
|
|
|
|
if err != nil {
|
|
|
|
log.Printf("FIXME: ignoring misssing glyph %v", r)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
out = append(out, oneRegion(pt, glyph.Image)...)
|
|
|
|
pt.X += glyph.Rect.Dx()
|
|
|
|
}
|
|
|
|
|
|
|
|
return out
|
|
|
|
}
|