Files
ordoor/internal/ui/noninteractive.go

341 lines
7.6 KiB
Go
Raw Normal View History

package ui
import (
"fmt"
"image"
2020-03-26 23:35:34 +00:00
"log"
2020-04-01 19:45:57 +01:00
"code.ur.gs/lupine/ordoor/internal/assetstore"
"code.ur.gs/lupine/ordoor/internal/menus"
)
2020-04-01 19:45:57 +01:00
type AlignMode int
const (
AlignModeCentre AlignMode = 0
AlignModeLeft AlignMode = 1
)
// 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 {
locator string
frames animation
rect image.Rectangle
// Some non-interactives, e.g., overlays, are an image + text to be shown
2020-04-01 19:45:57 +01:00
label *label
clickImpl // Alright, alright, it turns out the bridge mission briefing is clickable
hoverImpl
}
// Paint some text to screen, possibly settable
2020-04-01 19:45:57 +01:00
type label struct {
locator string
align AlignMode
rect image.Rectangle
font *assetstore.Font
valueImpl
2020-04-01 19:45:57 +01:00
}
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
}
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)
}
sprite, err := d.menu.Sprite(p.ObjectIdx, spriteId)
if err != nil {
2020-04-01 01:38:42 +01:00
return nil, err
}
ni := &noninteractive{
locator: p.Locator,
frames: animation{sprite.Image},
rect: sprite.Rect.Add(p.Point()),
}
2020-04-01 01:38:42 +01:00
return ni, nil
}
func (d *Driver) buildStatic(p *menus.Properties) (*noninteractive, *Widget, error) {
ni, err := d.buildNoninteractive(p)
if err != nil {
return nil, nil, err
}
ni.hoverImpl.text = p.Text
widget := &Widget{
Locator: ni.locator,
2020-04-19 18:21:08 +01:00
Active: p.Active,
ownClickables: []clickable{ni}, // FIXME: credits background needs to be clickable
ownHoverables: []hoverable{ni},
ownPaintables: []paintable{ni},
}
return ni, widget, nil
}
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,
2020-04-19 18:21:08 +01:00
Active: p.Active,
ownClickables: []clickable{ni},
ownHoverables: []hoverable{ni},
}
return ni, widget, nil
}
func (d *Driver) buildClickText(p *menus.Properties) (*noninteractive, *Widget, error) {
ni, err := d.buildNoninteractive(p)
if err != nil {
return nil, nil, err
}
fnt := d.menu.Font(p.FontType/10 - 1)
// FIXME: is this always right? Seems to make sense for Main.mnu
ni.label = &label{
locator: ni.locator,
font: fnt,
rect: ni.rect, // We will be centered by default
// Starts with no text. The text specified in the menu is hovertext
}
widget := &Widget{
Locator: ni.locator,
Active: p.Active,
ownClickables: []clickable{ni},
ownPaintables: []paintable{ni},
ownValueables: []valueable{ni.label},
}
return ni, widget, nil
}
// An overlay is a static image + some text that needs to be rendered
func (d *Driver) buildOverlay(p *menus.Properties) (*noninteractive, *Widget, error) {
ni, err := d.buildNoninteractive(p)
if err != nil {
return nil, nil, err
}
widget := &Widget{
Locator: ni.locator,
2020-04-19 18:21:08 +01:00
Active: p.Active,
ownPaintables: []paintable{ni},
}
if p.Text != "" {
// FIXME: is this always right? Seems to make sense for Main.mnu
fnt := d.menu.Font(p.FontType/10 - 1)
2020-04-01 19:45:57 +01:00
ni.label = &label{
font: fnt,
rect: ni.rect, // We will be centered by default
valueImpl: valueImpl{str: p.Text},
}
2020-03-26 23:35:34 +00:00
} else {
log.Printf("Overlay without text detected in %v", p.Locator)
}
return ni, widget, nil
}
// An animation is a non-interactive element that displays something in a loop
func (d *Driver) buildAnimationSample(p *menus.Properties) (*noninteractive, *Widget, error) {
sprite, err := d.menu.Sprite(p.ObjectIdx, p.SpriteId[0])
if err != nil {
return nil, nil, err
}
frames, err := d.menu.Images(p.ObjectIdx, p.SpriteId[0], p.DrawType)
if err != nil {
return nil, nil, err
}
ani := &noninteractive{
locator: p.Locator,
frames: animation(frames),
hoverImpl: hoverImpl{text: p.Text},
rect: sprite.Rect.Add(p.Point()),
}
widget := &Widget{
2020-04-19 18:21:08 +01:00
Active: p.Active,
ownHoverables: []hoverable{ani},
ownPaintables: []paintable{ani},
}
return ani, widget, nil
}
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 {
return nil, nil, err
2020-03-27 00:54:57 +00:00
}
enterFrames, err := d.menu.Images(p.ObjectIdx, p.SpriteId[0], p.DrawType)
2020-03-27 00:54:57 +00:00
if err != nil {
return nil, nil, err
2020-03-27 00:54:57 +00: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 {
return nil, nil, err
2020-03-27 00:54:57 +00:00
}
ani := &animationHover{
noninteractive: noninteractive{
locator: p.Locator,
2020-03-27 00:54:57 +00:00
frames: animation(enterFrames),
hoverImpl: hoverImpl{text: p.Text},
rect: sprite.Rect.Add(p.Point()),
2020-03-27 00:54:57 +00:00
},
exitFrames: animation(exitFrames),
}
widget := &Widget{
2020-04-19 18:21:08 +01:00
Active: p.Active,
ownHoverables: []hoverable{ani},
ownPaintables: []paintable{ani},
}
2020-03-27 00:54:57 +00:00
return ani, widget, nil
2020-03-27 00:54:57 +00:00
}
func (n *noninteractive) id() string {
return n.locator
}
func (n *noninteractive) bounds() image.Rectangle {
return n.rect
}
func (n *noninteractive) regions(tick int) []region {
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)...)
}
return out
}
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
func (l *label) id() string {
return l.locator
}
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.str)
2020-04-01 19:45:57 +01:00
// 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.str {
2020-06-13 18:23:50 +01:00
var sprite *assetstore.Sprite
if glyph, err := l.font.Glyph(r); err != nil {
if glyph, err := l.font.Glyph('?'); err != nil {
log.Printf("FIXME: ignoring glyph %v", r)
continue
} else {
sprite = glyph
}
} else {
sprite = glyph
2020-04-01 19:45:57 +01:00
}
2020-06-13 18:23:50 +01:00
out = append(out, oneRegion(pt, sprite.Image)...)
pt.X += sprite.Rect.Dx()
2020-04-01 19:45:57 +01:00
}
return out
}