Make the menu buttons work

This commit is contained in:
2020-03-21 18:50:26 +00:00
parent be4229b8fe
commit 46925c09d1
5 changed files with 194 additions and 66 deletions

View File

@@ -120,7 +120,7 @@ func (e *env) Draw(screen *ebiten.Image) error {
cam.Scale(scaleX, scaleY) cam.Scale(scaleX, scaleY)
for _, record := range e.menu.Records { for _, record := range e.menu.Records {
if err := e.drawRecord(record, screen, cam); err != nil { if err := e.drawRecordRecursive(record, screen, cam); err != nil {
return err return err
} }
} }
@@ -128,33 +128,57 @@ func (e *env) Draw(screen *ebiten.Image) error {
return nil return nil
} }
func (e *env) drawRecord(record *menus.Record, screen *ebiten.Image, offset ebiten.GeoM) error { func (e *env) drawRecordRecursive(record *menus.Record, screen *ebiten.Image, geo ebiten.GeoM) error {
if err := e.drawRecord(record, screen, geo); err != nil {
return err
}
// Draw all children of this record
for _, child := range record.Children {
if err := e.drawRecordRecursive(child, screen, geo); err != nil {
return err
}
}
return nil
}
// If the record has a "share" type, we can work out whether it's
func (e *env) isFocused(record *menus.Record, geo ebiten.GeoM) bool {
if record.Share < 0 {
return false
}
sprite, err := e.objects[0].Sprite(record.Share) // FIXME: need to handle multiple objects
if err != nil {
return false
}
invGeo := geo
invGeo.Invert()
cX, cY := ebiten.CursorPosition()
cursorX, cursorY := invGeo.Apply(float64(cX), float64(cY)) // Undo screen scaling
cursorPoint := image.Pt(int(cursorX), int(cursorY))
return cursorPoint.In(sprite.Rect)
}
func (e *env) drawRecord(record *menus.Record, screen *ebiten.Image, geo ebiten.GeoM) error {
// Draw this record if it's valid to do so. FIXME: lots to learn // Draw this record if it's valid to do so. FIXME: lots to learn
if len(record.SpriteId) >= 0 {
spriteId := record.SpriteId[0]
x := float64(record.X)
y := float64(record.Y)
// Maybe: we either give spriteid, or y,x,spriteId ? Unsure, doesn't seem spriteId := record.SelectSprite(
// to be needed for now e.step/2,
if len(record.SpriteId) == 3 { ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft),
// x = x + float64(record.SpriteId[1]) e.isFocused(record, geo),
// y = y + float64(record.SpriteId[0]) )
spriteId = record.SpriteId[2]
}
// FIXME: some here are set at -1. Presume that means don't draw.
if spriteId < 0 { if spriteId < 0 {
goto out return nil
} }
// FIXME: some are set at -1, -1. No idea why. Origin? // X-CORD and Y-CORD are universally either 0 or -1, so ignore here.
if x < 0.0 { // TODO: maybe 0 overrides in-sprite offset (set below)?
x = 0.0
}
if y < 0.0 {
y = 0.0
}
// FIXME: Need to handle multiple objects // FIXME: Need to handle multiple objects
obj := e.objects[0] obj := e.objects[0]
@@ -163,32 +187,23 @@ func (e *env) drawRecord(record *menus.Record, screen *ebiten.Image, offset ebit
return err return err
} }
x = x + float64(sprite.XOffset) // Account for scaling, draw sprite at its specified offset
y = y + float64(sprite.YOffset) x, y := geo.Apply(float64(sprite.XOffset), float64(sprite.YOffset))
// Account for scaling
x, y = offset.Apply(x, y)
log.Printf( // log.Printf(
"Drawing id=%v type=%v spriteid=%v x=%v(+%v) y=%v(%+v) desc=%q parent=%p", // "Drawing id=%v type=%v spriteid=%v x=%v(+%v) y=%v(%+v) desc=%q parent=%p",
record.Id, record.Type, spriteId, record.X, record.Y, sprite.XOffset, sprite.YOffset, record.Desc, record.Parent, // record.Id, record.Type, spriteId, record.X, record.Y, sprite.XOffset, sprite.YOffset, record.Desc, record.Parent,
) // )
offset.Translate(x, y) geo.Translate(x, y)
screen.DrawImage(sprite.Image, &ebiten.DrawImageOptions{GeoM: offset})
screen.DrawImage(sprite.Image, &ebiten.DrawImageOptions{GeoM: geo})
// FIXME: we probably shouldn't draw everything? // FIXME: we probably shouldn't draw everything?
// FIXME: handle multiple fonts // FIXME: handle multiple fonts
// if len(e.fonts) > 0 && record.Desc != "" { // if len(e.fonts) > 0 && record.Desc != "" {
// e.fonts[0].Output(screen, origOffset, record.Desc) // e.fonts[0].Output(screen, origOffset, record.Desc)
// } // }
}
out:
// Draw all children of this record
for _, child := range record.Children {
if err := e.drawRecord(child, screen, offset); err != nil {
return err
}
}
return nil return nil
} }

View File

@@ -15,7 +15,8 @@ import (
var ( var (
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation") gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation")
objName = flag.String("obj", "", "Name of an .obj file, e.g. TZEENTCH") objFile = flag.String("obj-file", "", "Path of an .obj file, e.g. ./orig/Obj/TZEENTCH.OBJ")
objName = flag.String("obj-name", "", "Name of an .obj file, e.g. TZEENTCH")
) )
type env struct { type env struct {
@@ -36,7 +37,7 @@ type state struct {
func main() { func main() {
flag.Parse() flag.Parse()
if *gamePath == "" || *objName == "" { if *gamePath == "" || (*objName == "" && *objFile == "") {
flag.Usage() flag.Usage()
os.Exit(1) os.Exit(1)
} }
@@ -46,9 +47,14 @@ func main() {
log.Fatal("Failed to set up asset store: %v", err) log.Fatal("Failed to set up asset store: %v", err)
} }
obj, err := assets.Object(*objName) var obj *assetstore.Object
if *objName != "" {
obj, err = assets.Object(*objName)
} else {
obj, err = assets.ObjectByPath(*objFile)
}
if err != nil { if err != nil {
log.Fatalf("Failed to load %s: %v", *objName, err) log.Fatalf("Failed to load %s%s: %v", *objName, *objFile, err)
} }
state := state{ state := state{

View File

@@ -201,3 +201,64 @@ This seems to be done by choosing a different sprite to draw every N ticks. They
are laid out sequentially, but I don't yet know how to animate them. It's likely are laid out sequentially, but I don't yet know how to animate them. It's likely
to be the same approach as used for other obj files. to be the same approach as used for other obj files.
Looking at Main.mnu, it points at the object fail Main.obj. This has 118
sprites, which can be described as follows:
| Start | Count | Desc |
| ------ | ----- | ---- |
| 0 | 1 | Background image |
| 1 | 3 | New game button: base, pressed, disabled |
| 4 | 3 | Load game button: base, pressed, disabled |
| 7 | 3 | Multiplayer button: base, pressed, disabled |
| 10 | 3 | Settings button: base, pressed, disabled |
| 13 | 3 | Quit button: base, pressed, disabled |
| 16 | 20 | New game button: 20 animation frames |
| 36 | 20 | Load game button: 20 animation frames |
| 56 | 20 | Multiplayer button: 20 animation frames |
| 76 | 20 | Settings button: 20 animation frames |
| 96 | 20 | Quit button: 20 animation frames |
| 116 | 1 | Section of background ("Menu title") |
| 117 | 1 | Version hotspot |
So we have 5 buttons with very similar characteristics, but at different sprite
offsets, and two distinct ranges per button, plus some others. Here's some
attributes plucked from `Main.mnu`:
| Name | (SUB)MENUTYPE | "Active" | "SPRITEID" | "DRAW TYPE" | "SHARE" |
| ---------- | ------------- | -------- | ---------- | ----------- | ------- |
| Background | 1 | 1 | 0 | 0 | -1 |
| Start menu | 1 | 1 | -1 | 0 | -1 |
| New game | 228 | 1,0 | 16,-1,1 | 20 | 1 |
| Load game | 228 | 1,0 | 36,-1,4 | 20 | 4 |
| MP game | 228 | 1,0 | 56,-1,7 | 20 | 7 |
| Options | 228 | 1,0 | 76,-1,10 | 20 | 10 |
| Quit | 228 | 1,0 | 96,-1,13 | 20 | 13 |
| Menu title | 61 | 1 | -1 | 0 | 116 |
| V hotspot | 61 | 1 | -1 | 0 | 117 |
The buttons, menu title and version hotspot are submenus of the start menu.
### `MENUTYPE`
This is the only menu where we see a type of 228. ~750 other unique values are
observed, suggesting structure. For instance, we have `24`, `240`, `241` and
`2410`, but not `2411` or `2409`. Sometimes we have a comma-separated list,
e.g.: `400,30,-1,5`.
### `ACTIVE`
There are only 4 values seen across all menus: `0`, `1`, `1,0`, `102` and `1,1`.
Perhaps this represents possible states?
### Sprite selection
For the background (`MENUTYPE: 1`), this points simply at the sprite index in
the object file. For the start menu, it's `-1` (no sprite, I assume). For the
menu title and version hotspot (`MENUTYPE: 61`, it's `-1` too.
For the buttons, it's a list pointing to the start of the 20 animated frames,
`-1`, then the start of the 3 static frames.
`DRAW TYPE` is the number of animated frames. We only use the animated frames
when the button is focused. `SHARE` repeats the start of the static frames, and
is the only place they're found for the menu title and version hotspot.

View File

@@ -2,6 +2,7 @@ package assetstore
import ( import (
"fmt" "fmt"
"image"
"log" "log"
"path/filepath" "path/filepath"
"strings" "strings"
@@ -23,12 +24,13 @@ type Object struct {
type Sprite struct { type Sprite struct {
obj *Object obj *Object
ID string XOffset int // TODO: replace these everywhere with Rect
XOffset int
YOffset int YOffset int
Width int Width int
Height int Height int
ID string
Rect image.Rectangle
Image *ebiten.Image Image *ebiten.Image
} }
@@ -106,13 +108,21 @@ func (o *Object) Sprite(idx int) (*Sprite, error) {
return nil, err return nil, err
} }
rect := image.Rect(
int(raw.XOffset),
int(raw.YOffset),
int(raw.XOffset+raw.Width),
int(raw.YOffset+raw.Height),
)
sprite := &Sprite{ sprite := &Sprite{
ID: fmt.Sprintf("%v:%v", o.raw.Name, idx), ID: fmt.Sprintf("%v:%v", o.raw.Name, idx),
obj: o, obj: o,
Width: int(raw.Width), Width: rect.Dx(),
Height: int(raw.Width), Height: rect.Dy(),
XOffset: int(raw.XOffset), XOffset: rect.Min.X,
YOffset: int(raw.YOffset), YOffset: rect.Min.Y,
Rect: rect,
Image: img, Image: img,
} }

View File

@@ -10,15 +10,24 @@ import (
"code.ur.gs/lupine/ordoor/internal/util/asciiscan" "code.ur.gs/lupine/ordoor/internal/util/asciiscan"
) )
const (
TypeStatic = 0
TypeMenu = 1
TypeOverlay = 61
TypeMainButton = 228
)
type Record struct { type Record struct {
Parent *Record Parent *Record
Children []*Record Children []*Record
Id int Id int
Type int Type int
DrawType int
FontType int FontType int
Active bool Active bool
SpriteId []int SpriteId []int
Share int
X int X int
Y int Y int
Desc string Desc string
@@ -161,6 +170,29 @@ func (r *Record) Toplevel() *Record {
return r return r
} }
func (r *Record) SelectSprite(step int, pressed, focused bool) int {
switch r.Type {
case TypeStatic:
return r.SpriteId[0]
case TypeMenu:
return r.SpriteId[0] // Probably -1
case TypeOverlay:
return r.Share
case TypeMainButton:
// A main button has 4 states: unfocused, focused (animated), mousedown, disabled
if focused && pressed {
return r.Share + 1
} else if focused {
return r.SpriteId[0] + (step % r.DrawType)
}
// TODO: disabled
return r.Share
}
return -1
}
func setProperty(r *Record, k, v string) { func setProperty(r *Record, k, v string) {
vSplit := strings.Split(v, ",") vSplit := strings.Split(v, ",")
vInt, _ := strconv.Atoi(v) vInt, _ := strconv.Atoi(v)
@@ -187,6 +219,10 @@ func setProperty(r *Record, k, v string) {
r.Desc = v r.Desc = v
case "FONTTYPE": case "FONTTYPE":
r.FontType = vInt r.FontType = vInt
case "DRAW TYPE":
r.DrawType = vInt
case "SHARE":
r.Share = vInt
default: default:
r.properties[k] = v r.properties[k] = v
} }