Allow dialogues to be hidden or shown
To do this, MENU and SUBMENU are split into two types (at last), and a Widget type is introduced. This should allow lots of code to be removed at some point.
This commit is contained in:
@@ -157,24 +157,24 @@ func loadMenus() {
|
|||||||
|
|
||||||
for _, menu := range menus {
|
for _, menu := range menus {
|
||||||
fmt.Printf(" * `%s`: objects=%v fonts=%v\n", menu.Name, menu.ObjectFiles, menu.FontNames)
|
fmt.Printf(" * `%s`: objects=%v fonts=%v\n", menu.Name, menu.ObjectFiles, menu.FontNames)
|
||||||
for _, record := range menu.Records {
|
|
||||||
displayRecord(record, 2)
|
for _, group := range menu.Groups {
|
||||||
|
// TODO: display group
|
||||||
|
for _, record := range group.Records {
|
||||||
|
displayRecord(record, 2)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func displayRecord(record *menus.Record, depth int) {
|
func displayRecord(record *menus.Record, depth int) {
|
||||||
content := fmt.Sprintf("id=%v type=%v sprite=%v", record.Id, record.Type, record.SpriteId)
|
content := fmt.Sprintf("id=%v type=%v sprite=%v", record.ID, record.Type, record.SpriteId)
|
||||||
|
|
||||||
if !record.Active {
|
if !record.Active {
|
||||||
content = "(" + content + ")"
|
content = "(" + content + ")"
|
||||||
}
|
}
|
||||||
|
|
||||||
fmt.Printf("%s* %s\n", strings.Repeat(" ", depth), content)
|
fmt.Printf("%s* %s\n", strings.Repeat(" ", depth), content)
|
||||||
|
|
||||||
for _, child := range record.Children {
|
|
||||||
displayRecord(child, depth+1)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func loadFonts() {
|
func loadFonts() {
|
||||||
|
@@ -5,6 +5,8 @@ import (
|
|||||||
"log"
|
"log"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
|
"github.com/hajimehoshi/ebiten"
|
||||||
|
|
||||||
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
||||||
"code.ur.gs/lupine/ordoor/internal/ui"
|
"code.ur.gs/lupine/ordoor/internal/ui"
|
||||||
)
|
)
|
||||||
@@ -17,6 +19,12 @@ var (
|
|||||||
winY = flag.Int("win-y", 1024, "Pre-scaled window Y dimension")
|
winY = flag.Int("win-y", 1024, "Pre-scaled window Y dimension")
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type dlg struct {
|
||||||
|
driver *ui.Driver
|
||||||
|
list []string
|
||||||
|
pos int
|
||||||
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
|
|
||||||
@@ -45,7 +53,38 @@ func main() {
|
|||||||
log.Fatal("Couldn't create window: %v", err)
|
log.Fatal("Couldn't create window: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Change the active dialogue
|
||||||
|
dialogues := driver.Dialogues()
|
||||||
|
if len(dialogues) > 0 {
|
||||||
|
dlg := &dlg{
|
||||||
|
driver: driver,
|
||||||
|
list: dialogues,
|
||||||
|
}
|
||||||
|
win.OnKeyUp(ebiten.KeyLeft, dlg.changeDialogue(-1))
|
||||||
|
win.OnKeyUp(ebiten.KeyRight, dlg.changeDialogue(+1))
|
||||||
|
for i, dialogue := range dlg.list {
|
||||||
|
log.Printf("Dialogue %v: %v", i, dialogue)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if err := win.Run(); err != nil {
|
if err := win.Run(); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *dlg) changeDialogue(by int) func() {
|
||||||
|
return func() {
|
||||||
|
newPos := d.pos + by
|
||||||
|
if newPos < 0 || newPos > len(d.list)-1 {
|
||||||
|
log.Printf("Hiding dialogue %v: %q", d.pos, d.list[d.pos])
|
||||||
|
d.driver.HideDialogue()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
locator := d.list[newPos]
|
||||||
|
log.Printf("Showing dialogue %v: %q", newPos, locator)
|
||||||
|
|
||||||
|
d.driver.ShowDialogue(locator)
|
||||||
|
d.pos = newPos
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -140,59 +140,71 @@ files.
|
|||||||
|
|
||||||
## `MENUTYPE`
|
## `MENUTYPE`
|
||||||
|
|
||||||
Here's the full list of
|
Here's the full list of values for `MENUTYPE`:
|
||||||
|
|
||||||
|
| Value | Meaning |
|
||||||
|
| ----- | ------------ |
|
||||||
|
| 0 | `Background` |
|
||||||
|
| 1 | `Menu` |
|
||||||
|
| 2 | `DragMenu` |
|
||||||
|
| 3 | `RadioMenu` ??? - only seen in `LevelPly` and `LoadGame` around select-one items |
|
||||||
|
| 45 | `MainBackground` ??? - only seen in `MainGame` and `MainGameChaos` |
|
||||||
|
| 300 | `Dialogue` |
|
||||||
|
|
||||||
|
The `MENUTYPE` acts as a logical grouping of a set of objects onscreen, and
|
||||||
|
gives strong hints about how to handle their children.
|
||||||
|
|
||||||
## `SUBMENUTYPE`
|
## `SUBMENUTYPE`
|
||||||
|
|
||||||
## (Sub)menu types
|
|
||||||
|
|
||||||
The types seem to refer to different types of UI widget. Here's a list of unique
|
The types seem to refer to different types of UI widget. Here's a list of unique
|
||||||
values:
|
values:
|
||||||
|
|
||||||
|
|
||||||
| Value | Meaning |
|
| Value | Meaning |
|
||||||
|-------|---------|
|
|-------|---------|
|
||||||
| 0 | Background |
|
| 3 | `Button` |
|
||||||
| 1 | Logical menu grouping? |
|
| 30 | `DoorHotspot1` |
|
||||||
| 2 | ? |
|
| 31 | `DoorHotspot2` |
|
||||||
| 3 | Standard button? |
|
| 40 | `LineKbd` |
|
||||||
| 30 | Equipment? |
|
| 41 | `LineBriefing` |
|
||||||
| 31 | "Character helmet" / "Slot" |
|
| 45 | `Thumb` |
|
||||||
| 40 | "X Line Y" |
|
| 50 | `InvokeButton` |
|
||||||
| 41 | "X Line Y" |
|
| 60 | `DoorHotspot3` |
|
||||||
| 45 | ? |
|
| 61 | `Overlay` |
|
||||||
| 45,10,11,9 | ? |
|
| 70 | `Hypertext` |
|
||||||
| 45,11,12,10 | ? |
|
| 91 | `Checkbox` |
|
||||||
| 45,14,15,13 | ? |
|
| 100 | `EditBox` |
|
||||||
| 45,17,18,16 | ? |
|
| 110 | `InventorySelect` |
|
||||||
| 45,3,4,2 | ? |
|
| 120 | `RadioButton` |
|
||||||
| 45,5,6,4 | ? |
|
| 200 | `DropdownButton` |
|
||||||
| 45,6,7,5 | ? |
|
| 205 | `ComboBoxItem` |
|
||||||
| 45,7,8,6 | ? |
|
| 220 | `AnimationSample` |
|
||||||
| 45,8,9,7 | ? |
|
| 221 | `AnimationHover` |
|
||||||
| 45,9,10,8 | ? |
|
| 228 | `MainButton` |
|
||||||
| 50 | ? |
|
| 232 | `Slider` |
|
||||||
| 60 | Other text to display? (`UltEquip.mnu`) |
|
| 233 | `StatusBar` |
|
||||||
| 61 | Text to display |
|
| 400 | `ListBoxUp` |
|
||||||
| 70 | Hypertext to display |
|
| 405 | `ListBoxDown` |
|
||||||
| 91 | ? |
|
|
||||||
| 100 | ? |
|
`400`, `405`, and `45`, can all accept 4 values for `SUBMENUTYPE` in a
|
||||||
| 110 | ? |
|
comma-separated list. These records combine to form a `TListBox` control, with a
|
||||||
| 120 | ? |
|
number of visible slots that act as a viewport. There is a draggable vertical
|
||||||
| 200 | Drop-down button? |
|
slider (the "thumb") to show where in the full list the viewport is, and up +
|
||||||
| 205 | Single list box item? |
|
down buttons to move the position of the thumb by one, so it's feasible that
|
||||||
| 220 | Psyker power? |
|
these values tell us about the available steps.
|
||||||
| 221 | Page? |
|
|
||||||
| 228 | Big buttons in `Main.mnu` |
|
Here are the values in `Briefing.mnu`:
|
||||||
| 232 | ? |
|
|
||||||
| 233 | ? |
|
```
|
||||||
| 300 | Pop-up dialog box |
|
#rem..........List Box Menu
|
||||||
| 400,0,0,{8, 16} | ? |
|
MENUTYPE : 1 # List Box Menu
|
||||||
| 400,22,22,{2, 4, 5, 6, 7, 8, 9, 9, 10, 13, 16} | ? |
|
SUBMENUTYPE: 400,22,22,13 # Scroll Up
|
||||||
| 400,30,-1,5 | ? |
|
SUBMENUTYPE: 405,22,22,13 # Scroll Down
|
||||||
| 405,0,0,{8, 16} | ? |
|
SUBMENUTYPE: 45, 14,15,13 # Thumb
|
||||||
| 405,22,22,{2, 4, 5, 6, 7, 8, 9, 10, 13, 16} | ? |
|
```
|
||||||
| 405,30,-1,5 | ? |
|
|
||||||
|
There are 13 elements in this listbox, which sorts out the fourth number (but
|
||||||
|
what is it used for?). The other two need more investigation.
|
||||||
|
|
||||||
## Positioning
|
## Positioning
|
||||||
|
|
||||||
@@ -205,6 +217,9 @@ successfully, for instance:
|
|||||||
|
|
||||||

|

|
||||||
|
|
||||||
|
However, it's *not* sufficient to put all the items for `MainGame.mnu` in the
|
||||||
|
right place.
|
||||||
|
|
||||||
## Animation
|
## Animation
|
||||||
|
|
||||||
This seems to be done by choosing a different sprite to draw every N ticks. They
|
This seems to be done by choosing a different sprite to draw every N ticks. They
|
||||||
@@ -248,36 +263,6 @@ attributes plucked from `Main.mnu`:
|
|||||||
|
|
||||||
The buttons, menu title and version hotspot are submenus of the start menu.
|
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`.
|
|
||||||
|
|
||||||
A listing of some currently-known values:
|
|
||||||
|
|
||||||
| Value | Type |
|
|
||||||
| ----- | ---------------- |
|
|
||||||
| 0 | Static image |
|
|
||||||
| 1 | Menu |
|
|
||||||
| 3 | Button |
|
|
||||||
| 45 | Thumb |
|
|
||||||
| 50 | Invoke? Button? |
|
|
||||||
| 61 | "Overlay" |
|
|
||||||
| 70 | "Hypertext" |
|
|
||||||
| 91 | Checkbox |
|
|
||||||
| 220 | Animation sample |
|
|
||||||
| 228 | Main menu button |
|
|
||||||
| 232 | Slider |
|
|
||||||
|
|
||||||
Hypothesis: `MENUTYPE` and `SUBMENUTYPE` are actually distinct lists of values.
|
|
||||||
So far, I've been treating them as the same thing, but, e.g., `MainGame.mnu` has
|
|
||||||
a `MENUTYPE: 45` which is labelled "MAIN BACKGROUND", while `SUBMENUTYPE: 45`
|
|
||||||
is tentatively labelled a "thumb" and used in text boxes. There are also a few
|
|
||||||
cases where I've had to manually override the `MENUTYPE` because it coincides
|
|
||||||
with `Button`.
|
|
||||||
|
|
||||||
### `ACTIVE`
|
### `ACTIVE`
|
||||||
|
|
||||||
There are only 4 values seen across all menus: `0`, `1`, `1,0`, `102` and `1,1`.
|
There are only 4 values seen across all menus: `0`, `1`, `1,0`, `102` and `1,1`.
|
||||||
|
@@ -16,8 +16,8 @@ type Menu struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: don't expose this
|
// FIXME: don't expose this
|
||||||
func (m *Menu) Records() []*menus.Record {
|
func (m *Menu) Groups() []*menus.Group {
|
||||||
return m.raw.Records
|
return m.raw.Groups
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: don't expose this
|
// FIXME: don't expose this
|
||||||
|
@@ -4,7 +4,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"image/color"
|
"image/color"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -13,38 +12,44 @@ import (
|
|||||||
"code.ur.gs/lupine/ordoor/internal/util/asciiscan"
|
"code.ur.gs/lupine/ordoor/internal/util/asciiscan"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// MenuType tells us what sort of Group we have
|
||||||
type MenuType int
|
type MenuType int
|
||||||
|
|
||||||
|
// SubMenuType tells us what sort of Record we have
|
||||||
|
type SubMenuType int
|
||||||
|
|
||||||
const (
|
const (
|
||||||
TypeStatic MenuType = 0
|
TypeStatic MenuType = 0
|
||||||
TypeMenu MenuType = 1
|
TypeMenu MenuType = 1
|
||||||
TypeDragMenu MenuType = 2 // Only seen in Configure_Vehicle_{Chaos,Ultra}
|
TypeDragMenu MenuType = 2 // Only seen in Configure_Vehicle_{Chaos,Ultra}
|
||||||
|
TypeRadioMenu MenuType = 3 // ???
|
||||||
|
TypeMainBackground MenuType = 45 // ???
|
||||||
|
TypeDialogue MenuType = 300
|
||||||
|
|
||||||
TypeSimpleButton MenuType = 3
|
SubTypeSimpleButton SubMenuType = 3
|
||||||
TypeDoorHotspot MenuType = 30 // Like a button I guess? "FONTTYPE is animation speed"
|
SubTypeDoorHotspot1 SubMenuType = 30 // Like a button I guess? "FONTTYPE is animation speed"
|
||||||
TypeDoorHotspot2 MenuType = 31 // Seems like a duplicate of the above? What's different?
|
SubTypeDoorHotspot2 SubMenuType = 31 // Seems like a duplicate of the above? What's different?
|
||||||
TypeLineKbd MenuType = 40
|
SubTypeLineKbd SubMenuType = 40
|
||||||
TypeThumb MenuType = 45 // A "thumb" appears to be a vertical slider
|
SubTypeLineBriefing SubMenuType = 41
|
||||||
TypeLineBriefing MenuType = 41
|
SubTypeThumb SubMenuType = 45 // A "thumb" appears to be a vertical slider
|
||||||
TypeInvokeButton MenuType = 50
|
SubTypeInvokeButton SubMenuType = 50
|
||||||
TypeDoorHotspot3 MenuType = 60 // Maybe? Appears in Arrange.mnu
|
SubTypeDoorHotspot3 SubMenuType = 60 // Maybe? Appears in Arrange.mnu
|
||||||
TypeOverlay MenuType = 61
|
SubTypeOverlay SubMenuType = 61
|
||||||
TypeHypertext MenuType = 70
|
SubTypeHypertext SubMenuType = 70
|
||||||
TypeCheckbox MenuType = 91
|
SubTypeCheckbox SubMenuType = 91
|
||||||
TypeEditBox MenuType = 100
|
SubTypeEditBox SubMenuType = 100
|
||||||
TypeInventorySelect MenuType = 110
|
SubTypeInventorySelect SubMenuType = 110
|
||||||
TypeRadioButton MenuType = 120
|
SubTypeRadioButton SubMenuType = 120
|
||||||
TypeDropdownButton MenuType = 200
|
SubTypeDropdownButton SubMenuType = 200
|
||||||
TypeComboBoxItem MenuType = 205
|
SubTypeComboBoxItem SubMenuType = 205
|
||||||
TypeAnimationSample MenuType = 220
|
SubTypeAnimationSample SubMenuType = 220
|
||||||
TypeAnimationHover MenuType = 221 // FONTTYPE is animation speed. Only animate when hovered
|
SubTypeAnimationHover SubMenuType = 221 // FONTTYPE is animation speed. Only animate when hovered
|
||||||
TypeMainButton MenuType = 228
|
SubTypeMainButton SubMenuType = 228
|
||||||
TypeSlider MenuType = 232
|
SubTypeSlider SubMenuType = 232
|
||||||
TypeStatusBar MenuType = 233
|
SubTypeStatusBar SubMenuType = 233
|
||||||
TypeDialogue MenuType = 300
|
|
||||||
|
|
||||||
TypeListBoxUp MenuType = 400 // FIXME: these have multiple items in MENUTYPE
|
SubTypeListBoxUp SubMenuType = 400 // FIXME: these have multiple items in SUBMENUTYPE
|
||||||
TypeListBoxDown MenuType = 405
|
SubTypeListBoxDown SubMenuType = 405
|
||||||
)
|
)
|
||||||
|
|
||||||
// FIXME: certain elements - especially overlays - don't have a DESC specified
|
// FIXME: certain elements - especially overlays - don't have a DESC specified
|
||||||
@@ -64,46 +69,11 @@ var TextOverrides = map[string]string{
|
|||||||
"main:2.7": "0.1-ordoor",
|
"main:2.7": "0.1-ordoor",
|
||||||
}
|
}
|
||||||
|
|
||||||
// FIXME: The menu is specified as type 2 (button) in these cases, which is
|
var TypeOverrides = map[string]SubMenuType{
|
||||||
// weird. Make it a menu for now.
|
// FIXME: These are put down as simple buttons, but it's a *lot* easier to
|
||||||
//
|
// understand them as list box buttons.
|
||||||
// Hypothesis: MENUTYPE and SUBMENUTYPE are not equivalent?
|
"configure_ultequip:7.5": SubTypeListBoxUp,
|
||||||
var TypeOverrides = map[string]MenuType{
|
"configure_ultequip:7.6": SubTypeListBoxDown,
|
||||||
"levelply:2": TypeMenu,
|
|
||||||
"savegame:2": TypeMenu,
|
|
||||||
"loadgame:2": TypeMenu,
|
|
||||||
|
|
||||||
// "thumb" is not a background.
|
|
||||||
"maingame:2": TypeStatic,
|
|
||||||
|
|
||||||
// ???
|
|
||||||
"configure_ultequip:7.5": TypeListBoxUp,
|
|
||||||
"configure_ultequip:7.6": TypeListBoxDown,
|
|
||||||
}
|
|
||||||
|
|
||||||
type Record struct {
|
|
||||||
Menu *Menu
|
|
||||||
Parent *Record
|
|
||||||
Children []*Record
|
|
||||||
|
|
||||||
Id int
|
|
||||||
ObjectIdx int // Can be specified in MENUID, defaults to 0
|
|
||||||
|
|
||||||
Type MenuType
|
|
||||||
DrawType int
|
|
||||||
FontType int
|
|
||||||
Active bool
|
|
||||||
SpriteId []int
|
|
||||||
Share int
|
|
||||||
X int
|
|
||||||
Y int
|
|
||||||
|
|
||||||
// From i18n
|
|
||||||
Text string
|
|
||||||
Help string
|
|
||||||
|
|
||||||
// FIXME: turn these into first-class data
|
|
||||||
properties map[string]string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Menu struct {
|
type Menu struct {
|
||||||
@@ -120,7 +90,48 @@ type Menu struct {
|
|||||||
|
|
||||||
// The actual menu records. There are multiple top-level items. Submenus are
|
// The actual menu records. There are multiple top-level items. Submenus are
|
||||||
// only ever nested one deep.
|
// only ever nested one deep.
|
||||||
|
Groups []*Group
|
||||||
|
}
|
||||||
|
|
||||||
|
// Group represents an element with a MENUTYPE. It is part of a Menu and may
|
||||||
|
// have children.
|
||||||
|
type Group struct {
|
||||||
|
Menu *Menu
|
||||||
Records []*Record
|
Records []*Record
|
||||||
|
|
||||||
|
Properties
|
||||||
|
Type MenuType
|
||||||
|
}
|
||||||
|
|
||||||
|
type Record struct {
|
||||||
|
Menu *Menu
|
||||||
|
Group *Group
|
||||||
|
|
||||||
|
Properties
|
||||||
|
Type SubMenuType
|
||||||
|
}
|
||||||
|
|
||||||
|
type Properties struct {
|
||||||
|
Locator string // Not strictly a property. Set for tracking.
|
||||||
|
|
||||||
|
ID int
|
||||||
|
ObjectIdx int // Can be specified in MENUID, defaults to 0
|
||||||
|
|
||||||
|
Accelerator int
|
||||||
|
Active bool
|
||||||
|
Desc string
|
||||||
|
DrawType int
|
||||||
|
FontType int
|
||||||
|
Moveable bool
|
||||||
|
Share int
|
||||||
|
SoundType int
|
||||||
|
SpriteId []int
|
||||||
|
X int
|
||||||
|
Y int
|
||||||
|
|
||||||
|
// From i18n
|
||||||
|
Text string
|
||||||
|
Help string
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadMenu(filename string) (*Menu, error) {
|
func LoadMenu(filename string) (*Menu, error) {
|
||||||
@@ -218,7 +229,10 @@ func loadFonts(menu *Menu, scanner *asciiscan.Scanner) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func loadRecords(baseDir string, menu *Menu, scanner *asciiscan.Scanner) error {
|
func loadRecords(baseDir string, menu *Menu, scanner *asciiscan.Scanner) error {
|
||||||
var record *Record // We build records here and add them when complete
|
// We build things up line by line in these variables
|
||||||
|
var group *Group
|
||||||
|
var record *Record
|
||||||
|
var properties *Properties
|
||||||
|
|
||||||
for {
|
for {
|
||||||
str, err := scanner.ConsumeString()
|
str, err := scanner.ConsumeString()
|
||||||
@@ -242,25 +256,49 @@ func loadRecords(baseDir string, menu *Menu, scanner *asciiscan.Scanner) error {
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
switch str {
|
if str == "*" {
|
||||||
case "*":
|
|
||||||
if record != nil {
|
if record != nil {
|
||||||
menu.Records = append(menu.Records, record.Toplevel())
|
group.Records = append(group.Records, record)
|
||||||
|
record = nil
|
||||||
}
|
}
|
||||||
|
|
||||||
continue // NEXT RECORD
|
if group != nil {
|
||||||
case "~":
|
menu.Groups = append(menu.Groups, group)
|
||||||
return nil // THE END
|
group = nil
|
||||||
|
}
|
||||||
|
|
||||||
|
continue // New group
|
||||||
|
}
|
||||||
|
|
||||||
|
if str == "~" {
|
||||||
|
break // THE END
|
||||||
}
|
}
|
||||||
|
|
||||||
k, v := asciiscan.ConsumeProperty(str)
|
k, v := asciiscan.ConsumeProperty(str)
|
||||||
switch k {
|
switch strings.ToUpper(k) {
|
||||||
case "MENUID":
|
case "MENUID":
|
||||||
record = newRecord(menu, nil)
|
if group != nil {
|
||||||
|
menu.Groups = append(menu.Groups, group)
|
||||||
|
}
|
||||||
|
|
||||||
|
group = newGroup(menu, v)
|
||||||
|
properties = &group.Properties
|
||||||
case "SUBMENUID":
|
case "SUBMENUID":
|
||||||
record = newRecord(menu, record.Toplevel())
|
if record != nil {
|
||||||
|
group.Records = append(group.Records, record)
|
||||||
|
}
|
||||||
|
|
||||||
|
record = newRecord(group, v)
|
||||||
|
properties = &record.Properties
|
||||||
|
case "MENUTYPE":
|
||||||
|
group.setMenuType(v)
|
||||||
|
case "SUBMENUTYPE":
|
||||||
|
record.setSubMenuType(v)
|
||||||
|
default:
|
||||||
|
if err := properties.setProperty(k, v); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
}
|
}
|
||||||
setProperty(record, k, v)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -295,85 +333,94 @@ func LoadMenus(dir string) (map[string]*Menu, error) {
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func newRecord(menu *Menu, parent *Record) *Record {
|
func listOfInts(s string) []int {
|
||||||
out := &Record{
|
vSplit := strings.Split(s, ",")
|
||||||
Menu: menu,
|
|
||||||
Parent: parent,
|
|
||||||
properties: map[string]string{},
|
|
||||||
}
|
|
||||||
|
|
||||||
if parent != nil {
|
|
||||||
parent.Children = append(parent.Children, out)
|
|
||||||
}
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Record) Toplevel() *Record {
|
|
||||||
if r.Parent != nil {
|
|
||||||
return r.Parent.Toplevel()
|
|
||||||
}
|
|
||||||
|
|
||||||
return r
|
|
||||||
}
|
|
||||||
|
|
||||||
func setProperty(r *Record, k, v string) {
|
|
||||||
vSplit := strings.Split(v, ",")
|
|
||||||
vInt, _ := strconv.Atoi(v)
|
|
||||||
vSplitInt := make([]int, len(vSplit))
|
vSplitInt := make([]int, len(vSplit))
|
||||||
|
|
||||||
for i, subV := range vSplit {
|
for i, subV := range vSplit {
|
||||||
vSplitInt[i], _ = strconv.Atoi(subV)
|
vSplitInt[i], _ = strconv.Atoi(subV)
|
||||||
}
|
}
|
||||||
|
|
||||||
switch k {
|
return vSplitInt
|
||||||
case "MENUID":
|
}
|
||||||
// ObjectIdx can be specified in the MENUID. Only seen for .mni files
|
|
||||||
if strings.Contains(v, ",") && len(vSplitInt) >= 2 {
|
|
||||||
r.Id = vSplitInt[0]
|
|
||||||
r.ObjectIdx = vSplitInt[1]
|
|
||||||
} else {
|
|
||||||
r.Id = vInt
|
|
||||||
}
|
|
||||||
case "SUBMENUID":
|
|
||||||
if strings.Contains(v, ",") {
|
|
||||||
log.Printf("%v has an object index in SUBMENUID - surprising", r.Locator())
|
|
||||||
r.Id = vSplitInt[0]
|
|
||||||
r.ObjectIdx = vSplitInt[1]
|
|
||||||
} else {
|
|
||||||
r.Id = vInt
|
|
||||||
if r.Parent != nil { // Children seem to inherit from parents?
|
|
||||||
r.ObjectIdx = r.Parent.ObjectIdx
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case "MENUTYPE", "SUBMENUTYPE":
|
|
||||||
if strings.Contains(v, ",") {
|
|
||||||
r.Type = MenuType(vSplitInt[0]) // FIXME: what are the other values in this case?
|
|
||||||
} else {
|
|
||||||
r.Type = MenuType(vInt)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: Type override. Note that MENUID is specified first, so this works
|
func newGroup(menu *Menu, idStr string) *Group {
|
||||||
if override, ok := TypeOverrides[r.Locator()]; ok {
|
out := &Group{Menu: menu}
|
||||||
r.Type = override
|
|
||||||
}
|
// ObjectIdx can be specified in the MENUID. Only seen for .mni files
|
||||||
case "ACTIVE":
|
ints := listOfInts(idStr)
|
||||||
r.Active = (vInt != 0)
|
out.ID = ints[0]
|
||||||
case "SPRITEID":
|
if len(ints) > 1 {
|
||||||
r.SpriteId = vSplitInt
|
out.ObjectIdx = ints[1]
|
||||||
case "X-CORD":
|
|
||||||
r.X = vInt
|
|
||||||
case "Y-CORD":
|
|
||||||
r.Y = vInt
|
|
||||||
case "FONTTYPE":
|
|
||||||
r.FontType = vInt
|
|
||||||
case "DRAW TYPE":
|
|
||||||
r.DrawType = vInt
|
|
||||||
case "SHARE":
|
|
||||||
r.Share = vInt
|
|
||||||
default:
|
|
||||||
r.properties[k] = v
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
out.Locator = fmt.Sprintf("%v:%v", menu.Name, out.ID)
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func newRecord(group *Group, idStr string) *Record {
|
||||||
|
out := &Record{Group: group}
|
||||||
|
|
||||||
|
out.ID, _ = strconv.Atoi(idStr) // FIXME: we're ignoring conversion errors here
|
||||||
|
out.ObjectIdx = group.ObjectIdx // FIXME: we shouldn't *copy* this
|
||||||
|
|
||||||
|
out.Locator = fmt.Sprintf("%v.%v", group.Locator, out.ID)
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (g *Group) setMenuType(s string) {
|
||||||
|
v, _ := strconv.Atoi(s) // FIXME: conversion errors
|
||||||
|
g.Type = MenuType(v)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (r *Record) setSubMenuType(s string) {
|
||||||
|
// FIXME: Type overrides shouldn't be necessary!
|
||||||
|
if override, ok := TypeOverrides[r.Locator]; ok {
|
||||||
|
r.Type = override
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: what are the other types here? Related to list boxes?
|
||||||
|
ints := listOfInts(s)
|
||||||
|
r.Type = SubMenuType(ints[0])
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Properties) setProperty(k, v string) error {
|
||||||
|
ints := listOfInts(v)
|
||||||
|
vInt := ints[0]
|
||||||
|
asBool := (vInt != 0)
|
||||||
|
|
||||||
|
switch strings.ToUpper(k) {
|
||||||
|
case "ACCELERATOR":
|
||||||
|
p.Accelerator = vInt
|
||||||
|
case "ACTIVE":
|
||||||
|
p.Active = asBool
|
||||||
|
case "DESC":
|
||||||
|
p.Desc = v // Usually int, occasionally string
|
||||||
|
case "DRAW TYPE":
|
||||||
|
p.DrawType = vInt
|
||||||
|
case "FONTTYPE":
|
||||||
|
p.FontType = vInt
|
||||||
|
case "MOVEABLE":
|
||||||
|
p.Moveable = asBool
|
||||||
|
case "SOUNDTYPE":
|
||||||
|
p.SoundType = vInt
|
||||||
|
case "SPRITEID":
|
||||||
|
p.SpriteId = ints
|
||||||
|
case "X-CORD":
|
||||||
|
p.X = vInt
|
||||||
|
case "Y-CORD":
|
||||||
|
p.Y = vInt
|
||||||
|
case "SHARE":
|
||||||
|
p.Share = vInt
|
||||||
|
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("Unknown property for %v: %v=%v", p.Locator, k, v)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
type Replacer interface {
|
type Replacer interface {
|
||||||
@@ -382,43 +429,47 @@ type Replacer interface {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (r *Record) Internationalize(replacer Replacer) {
|
func (r *Record) Internationalize(replacer Replacer) {
|
||||||
if override, ok := TextOverrides[r.Locator()]; ok {
|
if override, ok := TextOverrides[r.Locator]; ok {
|
||||||
delete(r.properties, "DESC")
|
|
||||||
r.Text = override
|
r.Text = override
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if override, ok := DescOverrides[r.Locator()]; ok {
|
if override, ok := DescOverrides[r.Locator]; ok {
|
||||||
r.properties["DESC"] = strconv.Itoa(override)
|
r.Desc = strconv.Itoa(override)
|
||||||
}
|
}
|
||||||
|
|
||||||
id, err := strconv.Atoi(r.properties["DESC"])
|
id, err := strconv.Atoi(r.Desc)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
delete(r.properties, "DESC")
|
|
||||||
replacer.ReplaceText(id, &r.Text)
|
replacer.ReplaceText(id, &r.Text)
|
||||||
replacer.ReplaceHelp(id, &r.Help)
|
replacer.ReplaceHelp(id, &r.Help)
|
||||||
}
|
} else {
|
||||||
|
r.Text = r.Desc // Sometimes it's a string like "EQUIPMENT"
|
||||||
for _, child := range r.Children {
|
|
||||||
child.Internationalize(replacer)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *Menu) Internationalize(replacer Replacer) {
|
func (m *Menu) Internationalize(replacer Replacer) {
|
||||||
for _, record := range m.Records {
|
for _, group := range m.Groups {
|
||||||
record.Internationalize(replacer)
|
for _, record := range group.Records {
|
||||||
|
record.Internationalize(replacer)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *Record) Path() string {
|
func (g *Group) Props() *Properties {
|
||||||
var path []string
|
return &g.Properties
|
||||||
|
}
|
||||||
|
|
||||||
for rec := r; rec != nil; rec = rec.Parent {
|
func (r *Record) Props() *Properties {
|
||||||
path = append([]string{strconv.Itoa(rec.Id)}, path...)
|
return &r.Properties
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Properties) BaseSpriteID() int {
|
||||||
|
base := p.Share
|
||||||
|
|
||||||
|
// SpriteId takes precedence if present
|
||||||
|
if len(p.SpriteId) > 0 && p.SpriteId[0] >= 0 {
|
||||||
|
base = p.SpriteId[0]
|
||||||
}
|
}
|
||||||
|
|
||||||
return strings.Join(path, ".")
|
return base
|
||||||
}
|
|
||||||
|
|
||||||
func (r *Record) Locator() string {
|
|
||||||
return fmt.Sprintf("%v:%v", r.Menu.Name, r.Path())
|
|
||||||
}
|
}
|
||||||
|
@@ -7,20 +7,11 @@ import (
|
|||||||
"code.ur.gs/lupine/ordoor/internal/menus"
|
"code.ur.gs/lupine/ordoor/internal/menus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
|
||||||
registerBuilder(menus.TypeSimpleButton, noChildren(registerSimpleButton))
|
|
||||||
registerBuilder(menus.TypeInvokeButton, noChildren(registerInvokeButton))
|
|
||||||
registerBuilder(menus.TypeMainButton, noChildren(registerMainButton))
|
|
||||||
registerBuilder(menus.TypeDoorHotspot, noChildren(registerDoorHotspot))
|
|
||||||
registerBuilder(menus.TypeDoorHotspot2, noChildren(registerDoorHotspot))
|
|
||||||
registerBuilder(menus.TypeDoorHotspot3, noChildren(registerDoorHotspot))
|
|
||||||
}
|
|
||||||
|
|
||||||
// A button without hover animation
|
// A button without hover animation
|
||||||
// FIXME: Keyboard.mnu has TypeSimpleButton instances that seem to include a
|
// FIXME: Keyboard.mnu has TypeSimpleButton instances that seem to include a
|
||||||
// hover in the SpriteId field
|
// hover in the SpriteId field
|
||||||
type button struct {
|
type button struct {
|
||||||
path string
|
locator string
|
||||||
|
|
||||||
baseSpr *assetstore.Sprite
|
baseSpr *assetstore.Sprite
|
||||||
clickSpr *assetstore.Sprite
|
clickSpr *assetstore.Sprite
|
||||||
@@ -38,95 +29,89 @@ type mainButton struct {
|
|||||||
button
|
button
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerSimpleButton(d *Driver, r *menus.Record) error {
|
func (d *Driver) buildButton(p *menus.Properties) (*button, *Widget, error) {
|
||||||
_, err := registerButton(d, r, r.SpriteId[0])
|
sprites, err := d.menu.Sprites(p.ObjectIdx, p.BaseSpriteID(), 3) // base, pressed, disabled
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func registerInvokeButton(d *Driver, r *menus.Record) error {
|
|
||||||
_, err := registerButton(d, r, r.Share)
|
|
||||||
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func registerMainButton(d *Driver, r *menus.Record) error {
|
|
||||||
sprites, err := d.menu.Sprites(r.ObjectIdx, r.Share, 3) // base, pressed, disabled
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
hovers, err := d.menu.Images(r.ObjectIdx, r.SpriteId[0], r.DrawType)
|
btn := &button{
|
||||||
|
locator: p.Locator,
|
||||||
|
baseSpr: sprites[0],
|
||||||
|
clickSpr: sprites[1],
|
||||||
|
frozenSpr: sprites[2],
|
||||||
|
hoverImpl: hoverImpl{text: p.Text},
|
||||||
|
}
|
||||||
|
|
||||||
|
widget := &Widget{
|
||||||
|
ownClickables: []clickable{btn},
|
||||||
|
ownFreezables: []freezable{btn},
|
||||||
|
ownHoverables: []hoverable{btn},
|
||||||
|
ownPaintables: []paintable{btn},
|
||||||
|
}
|
||||||
|
|
||||||
|
return btn, widget, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) buildMainButton(p *menus.Properties) (*mainButton, *Widget, error) {
|
||||||
|
sprites, err := d.menu.Sprites(p.ObjectIdx, p.Share, 3) // base, pressed, disabled
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
hovers, err := d.menu.Images(p.ObjectIdx, p.SpriteId[0], p.DrawType)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
btn := &mainButton{
|
btn := &mainButton{
|
||||||
hoverAnim: animation(hovers),
|
hoverAnim: animation(hovers),
|
||||||
button: button{
|
button: button{
|
||||||
path: r.Path(),
|
locator: p.Locator,
|
||||||
baseSpr: sprites[0],
|
baseSpr: sprites[0],
|
||||||
clickSpr: sprites[1],
|
clickSpr: sprites[1],
|
||||||
frozenSpr: sprites[2],
|
frozenSpr: sprites[2],
|
||||||
hoverImpl: hoverImpl{text: r.Text},
|
hoverImpl: hoverImpl{text: p.Text},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
d.clickables = append(d.clickables, btn)
|
widget := &Widget{
|
||||||
d.freezables = append(d.freezables, btn)
|
ownClickables: []clickable{btn},
|
||||||
d.hoverables = append(d.hoverables, btn)
|
ownFreezables: []freezable{btn},
|
||||||
d.paintables = append(d.paintables, btn)
|
ownHoverables: []hoverable{btn},
|
||||||
|
ownPaintables: []paintable{btn},
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return btn, widget, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerDoorHotspot(d *Driver, r *menus.Record) error {
|
func (d *Driver) buildDoorHotspot(p *menus.Properties) (*button, *Widget, error) {
|
||||||
sprites, err := d.menu.Sprites(r.ObjectIdx, r.Share, 2) // base, pressed
|
sprites, err := d.menu.Sprites(p.ObjectIdx, p.Share, 2) // base, pressed
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
btn := &button{
|
btn := &button{
|
||||||
path: r.Path(),
|
locator: p.Locator,
|
||||||
baseSpr: sprites[0],
|
baseSpr: sprites[0],
|
||||||
clickSpr: sprites[1],
|
clickSpr: sprites[1],
|
||||||
frozenSpr: sprites[0], // No disabled sprite
|
frozenSpr: sprites[0], // No disabled sprite
|
||||||
hoverImpl: hoverImpl{text: r.Text},
|
hoverImpl: hoverImpl{text: p.Text},
|
||||||
}
|
}
|
||||||
|
|
||||||
d.clickables = append(d.clickables, btn)
|
widget := &Widget{
|
||||||
d.freezables = append(d.freezables, btn)
|
ownClickables: []clickable{btn},
|
||||||
d.hoverables = append(d.hoverables, btn)
|
ownFreezables: []freezable{btn},
|
||||||
d.paintables = append(d.paintables, btn)
|
ownHoverables: []hoverable{btn},
|
||||||
|
ownPaintables: []paintable{btn},
|
||||||
return nil
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
func registerButton(d *Driver, r *menus.Record, spriteId int) (*button, error) {
|
|
||||||
sprites, err := d.menu.Sprites(r.ObjectIdx, spriteId, 3) // base, pressed, disabled
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
btn := &button{
|
return btn, widget, nil
|
||||||
path: r.Path(),
|
|
||||||
baseSpr: sprites[0],
|
|
||||||
clickSpr: sprites[1],
|
|
||||||
frozenSpr: sprites[2],
|
|
||||||
hoverImpl: hoverImpl{text: r.Text},
|
|
||||||
}
|
|
||||||
|
|
||||||
d.clickables = append(d.clickables, btn)
|
|
||||||
d.freezables = append(d.freezables, btn)
|
|
||||||
d.hoverables = append(d.hoverables, btn)
|
|
||||||
d.paintables = append(d.paintables, btn)
|
|
||||||
|
|
||||||
return btn, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *button) id() string {
|
func (b *button) id() string {
|
||||||
return b.path
|
return b.locator
|
||||||
}
|
}
|
||||||
|
|
||||||
func (b *button) bounds() image.Rectangle {
|
func (b *button) bounds() image.Rectangle {
|
||||||
|
@@ -1,20 +0,0 @@
|
|||||||
package ui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"code.ur.gs/lupine/ordoor/internal/menus"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
// Needed for Keyboard.mnu (main -> options -> keyboard).
|
|
||||||
// Dialogues can be active(?) or not. If they're not, then they are hidden.
|
|
||||||
// Dialogues seem to be modal in all cases?
|
|
||||||
registerBuilder(menus.TypeDialogue, registerDebug("WIP Dialogue", registerDialogue))
|
|
||||||
}
|
|
||||||
|
|
||||||
func registerDialogue(d *Driver, r *menus.Record) ([]*menus.Record, error) {
|
|
||||||
// The dialogue itself has a sprite
|
|
||||||
_, err := registerNoninteractive(d, r)
|
|
||||||
|
|
||||||
// TODO: we need to group these. Z levels seem overkill?
|
|
||||||
return r.Children, err
|
|
||||||
}
|
|
30
internal/ui/dialogues.go
Normal file
30
internal/ui/dialogues.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d *Driver) Dialogues() []string {
|
||||||
|
out := make([]string, len(d.dialogues))
|
||||||
|
|
||||||
|
for i, dialogue := range d.dialogues {
|
||||||
|
out[i] = dialogue.Locator
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) ShowDialogue(locator string) error {
|
||||||
|
for _, dialogue := range d.dialogues {
|
||||||
|
if dialogue.Locator == locator {
|
||||||
|
d.activeDialogue = dialogue
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return fmt.Errorf("Couldn't find dialogue %v", locator)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) HideDialogue() {
|
||||||
|
d.activeDialogue = nil
|
||||||
|
}
|
@@ -3,98 +3,40 @@ package ui
|
|||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"log"
|
|
||||||
"runtime/debug"
|
"runtime/debug"
|
||||||
"strconv"
|
|
||||||
|
|
||||||
"github.com/hajimehoshi/ebiten"
|
"github.com/hajimehoshi/ebiten"
|
||||||
"github.com/hajimehoshi/ebiten/ebitenutil"
|
"github.com/hajimehoshi/ebiten/ebitenutil"
|
||||||
|
|
||||||
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
"code.ur.gs/lupine/ordoor/internal/assetstore"
|
||||||
"code.ur.gs/lupine/ordoor/internal/menus"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
|
||||||
// FIXME: these need implementing
|
|
||||||
|
|
||||||
// Needed for MainGameChaos.mnu
|
|
||||||
registerBuilder(menus.TypeStatusBar, registerDebug("Unimplemented StatusBar", nil))
|
|
||||||
|
|
||||||
// Needed for Multiplayer_Choose.mnu
|
|
||||||
registerBuilder(menus.TypeComboBoxItem, registerDebug("Unimplemented ComboBoxItem", nil))
|
|
||||||
registerBuilder(menus.TypeDropdownButton, registerDebug("Unimplemented DropdownButton", nil))
|
|
||||||
|
|
||||||
// Needed for Multiplayer_Configure.mnu
|
|
||||||
registerBuilder(menus.TypeEditBox, registerDebug("Unimplemented EditBox", nil))
|
|
||||||
|
|
||||||
// Needed for Multiplayer_Connect.mnu
|
|
||||||
registerBuilder(menus.TypeRadioButton, registerDebug("Unimplemented RadioButton", nil))
|
|
||||||
}
|
|
||||||
|
|
||||||
const (
|
const (
|
||||||
OriginalX = 640.0
|
OriginalX = 640.0
|
||||||
OriginalY = 480.0
|
OriginalY = 480.0
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
|
||||||
// Widgets register their builder here
|
|
||||||
widgetBuilders = map[menus.MenuType]builderFunc{}
|
|
||||||
)
|
|
||||||
|
|
||||||
// Used to add widgets to a driver
|
|
||||||
type builderFunc func(d *Driver, r *menus.Record) (children []*menus.Record, err error)
|
|
||||||
|
|
||||||
func registerDebug(reason string, onward builderFunc) builderFunc {
|
|
||||||
return func(d *Driver, r *menus.Record) ([]*menus.Record, error) {
|
|
||||||
log.Printf("%v: %v: %#+v", reason, r.Locator(), r)
|
|
||||||
if onward == nil {
|
|
||||||
return r.Children, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return onward(d, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func noChildren(f func(d *Driver, r *menus.Record) error) builderFunc {
|
|
||||||
return func(d *Driver, r *menus.Record) ([]*menus.Record, error) {
|
|
||||||
if len(r.Children) > 0 {
|
|
||||||
return nil, fmt.Errorf("Children in record %v:%v (%#+v)", r.Menu.Name, r.Path(), r)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil, f(d, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func ownedByMenu(d *Driver, r *menus.Record) ([]*menus.Record, error) {
|
|
||||||
return nil, fmt.Errorf("This record should be handled by a menu: %v:%v (%#+v)", r.Menu.Name, r.Path(), r)
|
|
||||||
}
|
|
||||||
|
|
||||||
func registerBuilder(t menus.MenuType, f builderFunc) {
|
|
||||||
if _, ok := widgetBuilders[t]; ok {
|
|
||||||
panic(fmt.Sprintf("A builder for menu type %v already exists", t))
|
|
||||||
}
|
|
||||||
|
|
||||||
widgetBuilders[t] = f
|
|
||||||
}
|
|
||||||
|
|
||||||
// Driver acts as an interface between the main loop and the widgets specified
|
// Driver acts as an interface between the main loop and the widgets specified
|
||||||
// in a menu.
|
// in a menu.
|
||||||
//
|
//
|
||||||
// Menu assets assume a 640x480 screen; Driver is responsible for scaling to the
|
// Menu assets assume a 640x480 screen; Driver is responsible for scaling to the
|
||||||
// actual screen size when drawing.
|
// actual screen size when drawing.
|
||||||
|
//
|
||||||
|
// TODO: move scaling responsibilities to Window?
|
||||||
type Driver struct {
|
type Driver struct {
|
||||||
Name string
|
Name string
|
||||||
|
|
||||||
assets *assetstore.AssetStore
|
assets *assetstore.AssetStore
|
||||||
menu *assetstore.Menu
|
menu *assetstore.Menu
|
||||||
|
|
||||||
// UI elements we need to drive
|
// UI elements we need to drive. Note that widgets are hierarchical - these
|
||||||
clickables []clickable
|
// are just the toplevel. Dialogues are separated out. We only want to show
|
||||||
freezables []freezable
|
// one dialogue at a time, and if a dialogue is active, the main widgets are
|
||||||
hoverables []hoverable
|
// unusable (i.e., dialogues are modal)
|
||||||
mouseables []mouseable
|
dialogues []*Widget
|
||||||
paintables []paintable
|
widgets []*Widget
|
||||||
valueables []valueable
|
|
||||||
|
activeDialogue *Widget
|
||||||
|
|
||||||
cursor assetstore.CursorName
|
cursor assetstore.CursorName
|
||||||
|
|
||||||
@@ -118,8 +60,8 @@ func NewDriver(assets *assetstore.AssetStore, menu *assetstore.Menu) (*Driver, e
|
|||||||
menu: menu,
|
menu: menu,
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, record := range menu.Records() {
|
for _, group := range menu.Groups() {
|
||||||
if err := driver.addRecord(record); err != nil {
|
if err := driver.registerGroup(group); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -127,104 +69,6 @@ func NewDriver(assets *assetstore.AssetStore, menu *assetstore.Menu) (*Driver, e
|
|||||||
return driver, nil
|
return driver, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Driver) Value(id string, into *string) error {
|
|
||||||
for _, valueable := range d.valueables {
|
|
||||||
if valueable.id() == id {
|
|
||||||
*into = valueable.value()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("Couldn't find valueable widget %v:%v", d.menu.Name, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Driver) SetValue(id, value string) error {
|
|
||||||
for _, valueable := range d.valueables {
|
|
||||||
if valueable.id() == id {
|
|
||||||
valueable.setValue(value)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("Couldn't find valueable widget %v:%v", d.menu.Name, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Driver) ValueBool(id string, into *bool) error {
|
|
||||||
var vStr string
|
|
||||||
if err := d.Value(id, &vStr); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
*into = vStr == "1"
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Driver) SetValueBool(id string, value bool) error {
|
|
||||||
vStr := "0"
|
|
||||||
if value {
|
|
||||||
vStr = "1"
|
|
||||||
}
|
|
||||||
|
|
||||||
return d.SetValue(id, vStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Driver) SetFreeze(id string, value bool) error {
|
|
||||||
for _, freezable := range d.freezables {
|
|
||||||
if freezable.id() == id {
|
|
||||||
freezable.setFreezeState(value)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("Couldn't find clickable widget %v:%v", d.menu.Name, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Driver) OnClick(id string, f func()) error {
|
|
||||||
for _, clickable := range d.clickables {
|
|
||||||
if clickable.id() == id {
|
|
||||||
clickable.onClick(f)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("Couldn't find clickable widget %v:%v", d.menu.Name, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
// FIXME: HURK. Surely I'm missing something? steps is value:offset
|
|
||||||
func (d *Driver) ConfigureSlider(id string, steps map[int]int) error {
|
|
||||||
for _, clickable := range d.clickables {
|
|
||||||
if slider, ok := clickable.(*slider); ok && slider.id() == id {
|
|
||||||
slider.steps = steps
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return fmt.Errorf("Couldn't find slider %v:%v", d.menu.Name, id)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Driver) ValueInt(id string, into *int) error {
|
|
||||||
var vStr string
|
|
||||||
if err := d.Value(id, &vStr); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
value, err := strconv.Atoi(vStr)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
*into = value
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Driver) SetValueInt(id string, value int) error {
|
|
||||||
vStr := strconv.Itoa(value)
|
|
||||||
|
|
||||||
return d.SetValue(id, vStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (d *Driver) Update(screenX, screenY int) error {
|
func (d *Driver) Update(screenX, screenY int) error {
|
||||||
if d == nil {
|
if d == nil {
|
||||||
debug.PrintStack()
|
debug.PrintStack()
|
||||||
@@ -250,7 +94,7 @@ func (d *Driver) Update(screenX, screenY int) error {
|
|||||||
d.cursorOrig = image.Pt(int(mnX), int(mnY))
|
d.cursorOrig = image.Pt(int(mnX), int(mnY))
|
||||||
|
|
||||||
// Dispatch notifications to our widgets
|
// Dispatch notifications to our widgets
|
||||||
for _, hoverable := range d.hoverables {
|
for _, hoverable := range d.hoverables() {
|
||||||
inBounds := d.cursorOrig.In(hoverable.bounds())
|
inBounds := d.cursorOrig.In(hoverable.bounds())
|
||||||
|
|
||||||
d.hoverStartEvent(hoverable, inBounds)
|
d.hoverStartEvent(hoverable, inBounds)
|
||||||
@@ -262,7 +106,7 @@ func (d *Driver) Update(screenX, screenY int) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mouseIsDown := ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft)
|
mouseIsDown := ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft)
|
||||||
for _, clickable := range d.clickables {
|
for _, clickable := range d.clickables() {
|
||||||
inBounds := d.cursorOrig.In(clickable.bounds())
|
inBounds := d.cursorOrig.In(clickable.bounds())
|
||||||
mouseWasDown := clickable.mouseDownState()
|
mouseWasDown := clickable.mouseDownState()
|
||||||
|
|
||||||
@@ -271,7 +115,7 @@ func (d *Driver) Update(screenX, screenY int) error {
|
|||||||
d.mouseUpEvent(clickable, inBounds, mouseWasDown, mouseIsDown)
|
d.mouseUpEvent(clickable, inBounds, mouseWasDown, mouseIsDown)
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, mouseable := range d.mouseables {
|
for _, mouseable := range d.mouseables() {
|
||||||
mouseable.registerMousePosition(d.cursorOrig)
|
mouseable.registerMousePosition(d.cursorOrig)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,7 +130,7 @@ func (d *Driver) Draw(screen *ebiten.Image) error {
|
|||||||
|
|
||||||
var do ebiten.DrawImageOptions
|
var do ebiten.DrawImageOptions
|
||||||
|
|
||||||
for _, paint := range d.paintables {
|
for _, paint := range d.paintables() {
|
||||||
for _, region := range paint.regions(d.ticks) {
|
for _, region := range paint.regions(d.ticks) {
|
||||||
x, y := d.orig2native.Apply(float64(region.offset.X), float64(region.offset.Y))
|
x, y := d.orig2native.Apply(float64(region.offset.X), float64(region.offset.Y))
|
||||||
|
|
||||||
@@ -321,71 +165,66 @@ func (d *Driver) Cursor() (*ebiten.Image, *ebiten.DrawImageOptions, error) {
|
|||||||
return cursor.Image, op, nil
|
return cursor.Image, op, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Driver) addRecord(record *menus.Record) error {
|
func (d *Driver) clickables() []clickable {
|
||||||
//log.Printf("Adding record %v: %#+v", record.Locator(), record)
|
var out []clickable
|
||||||
children := record.Children
|
|
||||||
|
|
||||||
handler, ok := widgetBuilders[record.Type]
|
for _, widget := range d.widgets {
|
||||||
if !ok {
|
out = append(out, widget.clickables()...)
|
||||||
return fmt.Errorf("UI driver encountered unknown menu record: %#+v", record)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if handler != nil {
|
return out
|
||||||
var err error
|
|
||||||
children, err = handler(d, record)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Recursively add all remaining children of this record
|
|
||||||
for _, record := range children {
|
|
||||||
if err := d.addRecord(record); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Driver) hoverStartEvent(h hoverable, inBounds bool) {
|
func (d *Driver) freezables() []freezable {
|
||||||
if inBounds && !h.hoverState() {
|
var out []freezable
|
||||||
//log.Printf("hoverable false -> true")
|
|
||||||
h.setHoverState(true)
|
for _, widget := range d.widgets {
|
||||||
|
out = append(out, widget.freezables()...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Driver) hoverEndEvent(h hoverable, inBounds bool) {
|
func (d *Driver) hoverables() []hoverable {
|
||||||
if !inBounds && h.hoverState() {
|
var out []hoverable
|
||||||
//log.Printf("hoverable true -> false")
|
|
||||||
h.setHoverState(false)
|
for _, widget := range d.widgets {
|
||||||
|
out = append(out, widget.hoverables()...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Driver) mouseDownEvent(c clickable, inBounds, wasDown, isDown bool) {
|
func (d *Driver) mouseables() []mouseable {
|
||||||
if inBounds && !wasDown && isDown {
|
var out []mouseable
|
||||||
//log.Printf("mouse down false -> true")
|
|
||||||
c.setMouseDownState(true)
|
for _, widget := range d.widgets {
|
||||||
|
out = append(out, widget.mouseables()...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Driver) mouseClickEvent(c clickable, inBounds, wasDown, isDown bool) {
|
func (d *Driver) paintables() []paintable {
|
||||||
if inBounds && wasDown && !isDown {
|
var out []paintable
|
||||||
//log.Printf("mouse click")
|
|
||||||
c.registerMouseClick()
|
for _, widget := range d.widgets {
|
||||||
|
out = append(out, widget.paintables()...)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if d.activeDialogue != nil {
|
||||||
|
out = append(out, d.activeDialogue.paintables()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *Driver) mouseUpEvent(c clickable, inBounds, wasDown, isDown bool) {
|
func (d *Driver) valueables() []valueable {
|
||||||
if inBounds {
|
var out []valueable
|
||||||
if wasDown && !isDown {
|
|
||||||
//log.Printf("mouse down true -> false")
|
for _, widget := range d.widgets {
|
||||||
c.setMouseDownState(false)
|
out = append(out, widget.valueables()...)
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if wasDown {
|
|
||||||
//log.Printf("mouse down true -> false")
|
|
||||||
c.setMouseDownState(false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
}
|
}
|
||||||
|
43
internal/ui/events.go
Normal file
43
internal/ui/events.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
func (d *Driver) hoverStartEvent(h hoverable, inBounds bool) {
|
||||||
|
if inBounds && !h.hoverState() {
|
||||||
|
//log.Printf("hoverable false -> true")
|
||||||
|
h.setHoverState(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) hoverEndEvent(h hoverable, inBounds bool) {
|
||||||
|
if !inBounds && h.hoverState() {
|
||||||
|
//log.Printf("hoverable true -> false")
|
||||||
|
h.setHoverState(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) mouseDownEvent(c clickable, inBounds, wasDown, isDown bool) {
|
||||||
|
if inBounds && !wasDown && isDown {
|
||||||
|
//log.Printf("mouse down false -> true")
|
||||||
|
c.setMouseDownState(true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) mouseClickEvent(c clickable, inBounds, wasDown, isDown bool) {
|
||||||
|
if inBounds && wasDown && !isDown {
|
||||||
|
//log.Printf("mouse click")
|
||||||
|
c.registerMouseClick()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) mouseUpEvent(c clickable, inBounds, wasDown, isDown bool) {
|
||||||
|
if inBounds {
|
||||||
|
if wasDown && !isDown {
|
||||||
|
//log.Printf("mouse down true -> false")
|
||||||
|
c.setMouseDownState(false)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if wasDown {
|
||||||
|
//log.Printf("mouse down true -> false")
|
||||||
|
c.setMouseDownState(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
196
internal/ui/group.go
Normal file
196
internal/ui/group.go
Normal file
@@ -0,0 +1,196 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"code.ur.gs/lupine/ordoor/internal/menus"
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d *Driver) registerGroup(group *menus.Group) error {
|
||||||
|
// log.Printf("Adding group %v: %#+v", group.Locator, group)
|
||||||
|
|
||||||
|
var dialogue bool
|
||||||
|
|
||||||
|
switch group.Type {
|
||||||
|
case menus.TypeStatic, menus.TypeMainBackground, menus.TypeMenu, menus.TypeDragMenu, menus.TypeRadioMenu:
|
||||||
|
case menus.TypeDialogue:
|
||||||
|
dialogue = true
|
||||||
|
default:
|
||||||
|
return fmt.Errorf("Unknown group type: %v", group.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
var groupWidget *Widget
|
||||||
|
// Groups have a background sprite (FIXME: always?)
|
||||||
|
if group.BaseSpriteID() >= 0 {
|
||||||
|
var err error
|
||||||
|
_, groupWidget, err = d.buildStatic(group.Props())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
groupWidget = &Widget{Locator: group.Locator}
|
||||||
|
}
|
||||||
|
|
||||||
|
if dialogue {
|
||||||
|
d.dialogues = append(d.dialogues, groupWidget)
|
||||||
|
} else {
|
||||||
|
d.widgets = append(d.widgets, groupWidget)
|
||||||
|
}
|
||||||
|
|
||||||
|
// TRadioGroup is best handled like this
|
||||||
|
records, widget, err := d.maybeBuildInventorySelect(group, group.Records)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if widget != nil {
|
||||||
|
groupWidget.Children = append(groupWidget.Children, widget)
|
||||||
|
}
|
||||||
|
|
||||||
|
records, widget, err = d.maybeBuildListBox(group, records)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if widget != nil {
|
||||||
|
groupWidget.Children = append(groupWidget.Children, widget)
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, record := range records {
|
||||||
|
child, err := d.buildRecord(record)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if child != nil {
|
||||||
|
groupWidget.Children = append(groupWidget.Children, child)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) buildRecord(r *menus.Record) (*Widget, error) {
|
||||||
|
var widget *Widget
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch r.Type {
|
||||||
|
case menus.SubTypeSimpleButton, menus.SubTypeInvokeButton:
|
||||||
|
_, widget, err = d.buildButton(r.Props())
|
||||||
|
case menus.SubTypeDoorHotspot1, menus.SubTypeDoorHotspot2, menus.SubTypeDoorHotspot3:
|
||||||
|
_, widget, err = d.buildDoorHotspot(r.Props())
|
||||||
|
case menus.SubTypeOverlay:
|
||||||
|
_, widget, err = d.buildOverlay(r.Props())
|
||||||
|
case menus.SubTypeHypertext:
|
||||||
|
_, widget, err = d.buildHypertext(r.Props())
|
||||||
|
case menus.SubTypeCheckbox:
|
||||||
|
_, widget, err = d.buildCheckbox(r.Props())
|
||||||
|
case menus.SubTypeEditBox:
|
||||||
|
log.Printf("Unimplemented: SubTypeEditBox: %v", r.Locator) // TODO
|
||||||
|
case menus.SubTypeRadioButton:
|
||||||
|
log.Printf("Unimplemented: SubTypeRadioButton: %v", r.Locator) // TODO
|
||||||
|
case menus.SubTypeDropdownButton:
|
||||||
|
log.Printf("Unimplemented: SubTypeDropdownButton: %v", r.Locator) // TODO
|
||||||
|
case menus.SubTypeComboBoxItem:
|
||||||
|
log.Printf("Unimplemented: SubTypeComboBoxItem: %v", r.Locator) // TODO
|
||||||
|
case menus.SubTypeAnimationSample:
|
||||||
|
_, widget, err = d.buildAnimationSample(r.Props())
|
||||||
|
case menus.SubTypeAnimationHover:
|
||||||
|
_, widget, err = d.buildAnimationHover(r.Props())
|
||||||
|
case menus.SubTypeMainButton:
|
||||||
|
_, widget, err = d.buildMainButton(r.Props())
|
||||||
|
case menus.SubTypeSlider:
|
||||||
|
_, widget, err = d.buildSlider(r.Props()) // TODO: take sliders at an earlier point?
|
||||||
|
case menus.SubTypeStatusBar:
|
||||||
|
log.Printf("Unimplemented: SubTypeStatusBar: %v", r.Locator) // TODO
|
||||||
|
default:
|
||||||
|
return nil, fmt.Errorf("Unknown record type for %v: %v", r.Locator, r.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
return widget, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) maybeBuildInventorySelect(group *menus.Group, records []*menus.Record) ([]*menus.Record, *Widget, error) {
|
||||||
|
var untouched []*menus.Record
|
||||||
|
var touched []*menus.Record
|
||||||
|
|
||||||
|
for _, record := range records {
|
||||||
|
if record.Type == menus.SubTypeInventorySelect {
|
||||||
|
touched = append(touched, record)
|
||||||
|
} else {
|
||||||
|
untouched = append(untouched, record)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(touched) == 0 {
|
||||||
|
return untouched, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
elements := make([]*inventorySelect, len(touched))
|
||||||
|
widget := &Widget{
|
||||||
|
Locator: group.Locator,
|
||||||
|
}
|
||||||
|
|
||||||
|
for i, record := range touched {
|
||||||
|
element, childWidget, err := d.buildInventorySelect(record.Props())
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
elements[i] = element
|
||||||
|
widget.Children = append(widget.Children, childWidget)
|
||||||
|
}
|
||||||
|
|
||||||
|
elements[0].setValue("1")
|
||||||
|
|
||||||
|
for _, element := range elements {
|
||||||
|
element.others = elements
|
||||||
|
}
|
||||||
|
|
||||||
|
return untouched, widget, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) maybeBuildListBox(group *menus.Group, records []*menus.Record) ([]*menus.Record, *Widget, error) {
|
||||||
|
// Unless up, down, thumb, and items, are all present, it's not a listbox
|
||||||
|
var up *menus.Record
|
||||||
|
var down *menus.Record
|
||||||
|
var thumb *menus.Record
|
||||||
|
var items []*menus.Record
|
||||||
|
|
||||||
|
var untouched []*menus.Record
|
||||||
|
|
||||||
|
for _, rec := range records {
|
||||||
|
switch rec.Type {
|
||||||
|
case menus.SubTypeListBoxUp:
|
||||||
|
if up != nil {
|
||||||
|
return nil, nil, fmt.Errorf("Duplicate up buttons in menu %v", group.Locator)
|
||||||
|
}
|
||||||
|
up = rec
|
||||||
|
case menus.SubTypeListBoxDown:
|
||||||
|
if down != nil {
|
||||||
|
return nil, nil, fmt.Errorf("Duplicate down buttons in menu %v", group.Locator)
|
||||||
|
}
|
||||||
|
down = rec
|
||||||
|
case menus.SubTypeLineKbd, menus.SubTypeLineBriefing:
|
||||||
|
items = append(items, rec)
|
||||||
|
case menus.SubTypeThumb:
|
||||||
|
if thumb != nil {
|
||||||
|
return nil, nil, fmt.Errorf("Duplicate thumbs in menu %v", group.Locator)
|
||||||
|
}
|
||||||
|
thumb = rec
|
||||||
|
default:
|
||||||
|
// e.g. maingame:18.12 includes a button that is not part of the box
|
||||||
|
untouched = append(untouched, rec)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Since not all the elements are present, this isn't a listbox
|
||||||
|
if len(items) == 0 || thumb == nil || up == nil || down == nil {
|
||||||
|
return untouched, nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
_, widget, err := d.buildListBox(group, up, down, thumb, items...)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return untouched, widget, nil
|
||||||
|
}
|
@@ -4,10 +4,6 @@ import (
|
|||||||
"code.ur.gs/lupine/ordoor/internal/menus"
|
"code.ur.gs/lupine/ordoor/internal/menus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
|
||||||
registerBuilder(menus.TypeInventorySelect, ownedByMenu)
|
|
||||||
}
|
|
||||||
|
|
||||||
// An inventory select is a sort of radio button. If 2 share the same menu,
|
// An inventory select is a sort of radio button. If 2 share the same menu,
|
||||||
// selecting one deselects the other. Otherwise, they act like checkboxes.
|
// selecting one deselects the other. Otherwise, they act like checkboxes.
|
||||||
//
|
//
|
||||||
@@ -20,33 +16,18 @@ type inventorySelect struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Called from the menu, which fills "others" for us
|
// Called from the menu, which fills "others" for us
|
||||||
func registerInventorySelect(d *Driver, r *menus.Record) (*inventorySelect, error) {
|
func (d *Driver) buildInventorySelect(p *menus.Properties) (*inventorySelect, *Widget, error) {
|
||||||
sprites, err := d.menu.Sprites(r.ObjectIdx, r.Share, 3) // unchecked, checked, disabled
|
c, widget, err := d.buildCheckbox(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
element := &inventorySelect{
|
// In an inventorySelect, the frozen and click sprites are reversed
|
||||||
checkbox: checkbox{
|
c.clickSpr, c.frozenSpr = c.frozenSpr, c.clickSpr
|
||||||
button: button{
|
|
||||||
path: r.Path(),
|
|
||||||
baseSpr: sprites[0], // unchecked
|
|
||||||
clickSpr: sprites[1], // checked
|
|
||||||
frozenSpr: sprites[2], // disabled
|
|
||||||
hoverImpl: hoverImpl{text: r.Text},
|
|
||||||
},
|
|
||||||
|
|
||||||
valueImpl: valueImpl{str: "0"},
|
element := &inventorySelect{checkbox: *c}
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
d.clickables = append(d.clickables, element)
|
return element, widget, nil
|
||||||
d.freezables = append(d.freezables, element)
|
|
||||||
d.hoverables = append(d.hoverables, element)
|
|
||||||
d.paintables = append(d.paintables, element)
|
|
||||||
d.valueables = append(d.valueables, element)
|
|
||||||
|
|
||||||
return element, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (i *inventorySelect) registerMouseClick() {
|
func (i *inventorySelect) registerMouseClick() {
|
||||||
|
@@ -8,15 +8,6 @@ import (
|
|||||||
"code.ur.gs/lupine/ordoor/internal/menus"
|
"code.ur.gs/lupine/ordoor/internal/menus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
|
||||||
registerBuilder(menus.TypeLineKbd, ownedByMenu)
|
|
||||||
registerBuilder(menus.TypeLineBriefing, ownedByMenu)
|
|
||||||
|
|
||||||
registerBuilder(menus.TypeThumb, ownedByMenu)
|
|
||||||
registerBuilder(menus.TypeListBoxUp, ownedByMenu)
|
|
||||||
registerBuilder(menus.TypeListBoxDown, ownedByMenu)
|
|
||||||
}
|
|
||||||
|
|
||||||
// listBox is a TListBox in VCL terms. It has a number of lines of text, one of
|
// listBox is a TListBox in VCL terms. It has a number of lines of text, one of
|
||||||
// which may be selected, and a slider with up and down buttons to scroll if the
|
// which may be selected, and a slider with up and down buttons to scroll if the
|
||||||
// options in the box exceed its viewing capacity.
|
// options in the box exceed its viewing capacity.
|
||||||
@@ -30,7 +21,6 @@ type listBox struct {
|
|||||||
thumbBase *assetstore.Sprite // Bounds are given by this
|
thumbBase *assetstore.Sprite // Bounds are given by this
|
||||||
thumbImg *assetstore.Sprite // This is displayed at offset * (height / steps)
|
thumbImg *assetstore.Sprite // This is displayed at offset * (height / steps)
|
||||||
|
|
||||||
base *noninteractive // The menu itself has a sprite to display
|
|
||||||
lines []*noninteractive // We display to these
|
lines []*noninteractive // We display to these
|
||||||
|
|
||||||
// The list box acts as a window onto these
|
// The list box acts as a window onto these
|
||||||
@@ -40,87 +30,31 @@ type listBox struct {
|
|||||||
offset int
|
offset int
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerListBox(d *Driver, menu *menus.Record) ([]*menus.Record, error) {
|
func (d *Driver) buildListBox(group *menus.Group, up, down, thumb *menus.Record, items ...*menus.Record) (*listBox, *Widget, error) {
|
||||||
var upBtn *menus.Record
|
upElem, upWidget, err := d.buildButton(up.Props())
|
||||||
var downBtn *menus.Record
|
|
||||||
var thumb *menus.Record
|
|
||||||
var items []*menus.Record
|
|
||||||
var otherChildren []*menus.Record
|
|
||||||
|
|
||||||
for _, rec := range menu.Children {
|
|
||||||
switch rec.Type {
|
|
||||||
case menus.TypeListBoxUp:
|
|
||||||
if upBtn != nil {
|
|
||||||
return nil, fmt.Errorf("Duplicate up buttons in menu %v", menu.Locator())
|
|
||||||
}
|
|
||||||
upBtn = rec
|
|
||||||
case menus.TypeListBoxDown:
|
|
||||||
if downBtn != nil {
|
|
||||||
return nil, fmt.Errorf("Duplicate down buttons in menu %v", menu.Locator())
|
|
||||||
}
|
|
||||||
downBtn = rec
|
|
||||||
case menus.TypeLineKbd, menus.TypeLineBriefing:
|
|
||||||
items = append(items, rec)
|
|
||||||
case menus.TypeThumb:
|
|
||||||
if thumb != nil {
|
|
||||||
return nil, fmt.Errorf("Duplicate thumbs in menu %v", menu.Locator())
|
|
||||||
}
|
|
||||||
thumb = rec
|
|
||||||
default:
|
|
||||||
// e.g. maingame:18.12 includes a button that is not part of the box
|
|
||||||
otherChildren = append(otherChildren, rec)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(items) == 0 || thumb == nil || upBtn == nil || downBtn == nil {
|
|
||||||
return nil, fmt.Errorf("Missing items in menu %v", menu.Locator())
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now build the wonderful thing
|
|
||||||
baseElem, err := registerNoninteractive(d, menu)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
upSprId := upBtn.SpriteId[0]
|
downElem, downWidget, err := d.buildButton(down.Props())
|
||||||
if upSprId == -1 {
|
|
||||||
upSprId = upBtn.Share
|
|
||||||
}
|
|
||||||
|
|
||||||
elemUp, err := registerButton(d, upBtn, upSprId)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
dnSprId := downBtn.SpriteId[0]
|
thumbBaseSpr, err := d.menu.Sprite(thumb.ObjectIdx, thumb.Share)
|
||||||
if dnSprId == -1 {
|
|
||||||
dnSprId = downBtn.Share
|
|
||||||
}
|
|
||||||
elemDown, err := registerButton(d, downBtn, dnSprId)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
thumbBaseSpr, err := d.menu.Sprite(menu.ObjectIdx, thumb.Share)
|
thumbImgSpr, err := d.menu.Sprite(thumb.ObjectIdx, thumb.BaseSpriteID())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
|
||||||
|
|
||||||
thumbSprId := thumb.SpriteId[0]
|
|
||||||
if thumbSprId == -1 {
|
|
||||||
thumbSprId = thumb.Share
|
|
||||||
}
|
|
||||||
|
|
||||||
thumbImgSpr, err := d.menu.Sprite(menu.ObjectIdx, thumbSprId)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
element := &listBox{
|
element := &listBox{
|
||||||
base: baseElem,
|
|
||||||
// TODO: upBtn needs to be frozen when offset == 0; downBtn when offset == max
|
// TODO: upBtn needs to be frozen when offset == 0; downBtn when offset == max
|
||||||
upBtn: elemUp,
|
upBtn: upElem,
|
||||||
downBtn: elemDown,
|
downBtn: downElem,
|
||||||
|
|
||||||
// TODO: need to be able to drag the thumb
|
// TODO: need to be able to drag the thumb
|
||||||
thumbBase: thumbBaseSpr,
|
thumbBase: thumbBaseSpr,
|
||||||
@@ -128,8 +62,8 @@ func registerListBox(d *Driver, menu *menus.Record) ([]*menus.Record, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Internal wiring-up
|
// Internal wiring-up
|
||||||
elemUp.onClick(element.up)
|
upElem.onClick(element.up)
|
||||||
elemDown.onClick(element.down)
|
downElem.onClick(element.down)
|
||||||
|
|
||||||
// FIXME: Test data for now
|
// FIXME: Test data for now
|
||||||
for i := 0; i < 50; i++ {
|
for i := 0; i < 50; i++ {
|
||||||
@@ -138,30 +72,34 @@ func registerListBox(d *Driver, menu *menus.Record) ([]*menus.Record, error) {
|
|||||||
|
|
||||||
// Register everything. Since we're a composite of other controls, they are
|
// Register everything. Since we're a composite of other controls, they are
|
||||||
// mostly self-registered at the moment.
|
// mostly self-registered at the moment.
|
||||||
d.paintables = append(d.paintables, element)
|
widget := &Widget{
|
||||||
|
Children: []*Widget{upWidget, downWidget},
|
||||||
|
ownPaintables: []paintable{element},
|
||||||
|
}
|
||||||
|
|
||||||
// FIXME: we should be able to freeze/unfreeze as a group.
|
// FIXME: we should be able to freeze/unfreeze as a group.
|
||||||
|
|
||||||
// HURK: These need to be registered after the other elements so they are
|
// HURK: These need to be registered after the other elements so they are
|
||||||
// drawn in the correct order to be visible
|
// drawn in the correct order to be visible
|
||||||
for _, rec := range items {
|
for _, rec := range items {
|
||||||
ni, err := registerNoninteractive(d, rec)
|
ni, niWidget, err := d.buildStatic(rec.Props())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
// TODO: pick the correct font
|
// TODO: pick the correct font
|
||||||
ni.label = &label{
|
ni.label = &label{
|
||||||
align: AlignModeLeft,
|
align: AlignModeLeft,
|
||||||
font: d.menu.Font(0),
|
font: d.menu.Font(0),
|
||||||
rect: ni.rect,
|
rect: ni.rect,
|
||||||
}
|
}
|
||||||
|
|
||||||
element.lines = append(element.lines, ni)
|
element.lines = append(element.lines, ni)
|
||||||
|
widget.Children = append(widget.Children, niWidget)
|
||||||
}
|
}
|
||||||
|
|
||||||
element.refresh()
|
element.refresh()
|
||||||
|
|
||||||
return otherChildren, nil
|
return element, widget, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *listBox) SetStrings(to []string) {
|
func (l *listBox) SetStrings(to []string) {
|
||||||
|
@@ -1,72 +0,0 @@
|
|||||||
package ui
|
|
||||||
|
|
||||||
import (
|
|
||||||
"code.ur.gs/lupine/ordoor/internal/menus"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
// These menu types don't need driving, so we can ignore them
|
|
||||||
registerBuilder(menus.TypeMenu, registerMenu)
|
|
||||||
registerBuilder(menus.TypeDragMenu, nil) // Menus are just containers
|
|
||||||
}
|
|
||||||
|
|
||||||
func registerMenu(d *Driver, r *menus.Record) ([]*menus.Record, error) {
|
|
||||||
childrenLeft, err := listBoxFromMenu(d, r, r.Children)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
childrenLeft, err = inventorySelectFromMenu(d, r, childrenLeft)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Return all the unhandled children to be processed further
|
|
||||||
return childrenLeft, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func listBoxFromMenu(d *Driver, menu *menus.Record, children []*menus.Record) ([]*menus.Record, error) {
|
|
||||||
ok := false
|
|
||||||
for _, rec := range children {
|
|
||||||
if rec.Type == menus.TypeThumb { // FIXME: we're using this to indicate a listbox
|
|
||||||
ok = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if !ok {
|
|
||||||
return children, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return registerListBox(d, menu)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Group all inventory selects that share a menu together
|
|
||||||
func inventorySelectFromMenu(d *Driver, menu *menus.Record, children []*menus.Record) ([]*menus.Record, error) {
|
|
||||||
var childrenLeft []*menus.Record
|
|
||||||
var inventorySelects []*inventorySelect
|
|
||||||
|
|
||||||
for _, child := range children {
|
|
||||||
switch child.Type {
|
|
||||||
case menus.TypeInventorySelect:
|
|
||||||
is, err := registerInventorySelect(d, child)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
inventorySelects = append(inventorySelects, is)
|
|
||||||
default:
|
|
||||||
childrenLeft = append(childrenLeft, child)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(inventorySelects) > 0 {
|
|
||||||
inventorySelects[0].setValue("1") // Always start with one selected
|
|
||||||
|
|
||||||
for _, is := range inventorySelects {
|
|
||||||
is.others = inventorySelects
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return childrenLeft, nil
|
|
||||||
}
|
|
@@ -1,6 +1,7 @@
|
|||||||
package ui
|
package ui
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
"image"
|
"image"
|
||||||
"log"
|
"log"
|
||||||
|
|
||||||
@@ -15,22 +16,14 @@ const (
|
|||||||
AlignModeLeft AlignMode = 1
|
AlignModeLeft AlignMode = 1
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
|
||||||
registerBuilder(menus.TypeStatic, registerStatic) // MainGame has a hypertext child
|
|
||||||
registerBuilder(menus.TypeHypertext, noChildren(registerHypertext))
|
|
||||||
registerBuilder(menus.TypeOverlay, noChildren(registerOverlay))
|
|
||||||
registerBuilder(menus.TypeAnimationSample, noChildren(registerAnimation))
|
|
||||||
registerBuilder(menus.TypeAnimationHover, noChildren(registerAnimationHover))
|
|
||||||
}
|
|
||||||
|
|
||||||
// A non-interactive element is not a widget; it merely displays some pixels and
|
// A non-interactive element is not a widget; it merely displays some pixels and
|
||||||
// may optionally have a tooltip for display within bounds.
|
// may optionally have a tooltip for display within bounds.
|
||||||
//
|
//
|
||||||
// For non-animated non-interactive elements, just give them a single frame.
|
// For non-animated non-interactive elements, just give them a single frame.
|
||||||
type noninteractive struct {
|
type noninteractive struct {
|
||||||
path string
|
locator string
|
||||||
frames animation
|
frames animation
|
||||||
rect image.Rectangle
|
rect image.Rectangle
|
||||||
|
|
||||||
// Some non-interactives, e.g., overlays, are an image + text to be shown
|
// Some non-interactives, e.g., overlays, are an image + text to be shown
|
||||||
label *label
|
label *label
|
||||||
@@ -58,145 +51,153 @@ type animationHover struct {
|
|||||||
closing bool
|
closing bool
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerStatic(d *Driver, r *menus.Record) ([]*menus.Record, error) {
|
func (d *Driver) buildNoninteractive(p *menus.Properties) (*noninteractive, error) {
|
||||||
_, err := registerNoninteractive(d, r)
|
// FIXME: SpriteID takes precedence over SHARE if present, but is that
|
||||||
return r.Children, err
|
// always right?
|
||||||
}
|
spriteId := p.BaseSpriteID()
|
||||||
|
if spriteId < 0 {
|
||||||
func registerNoninteractive(d *Driver, r *menus.Record) (*noninteractive, error) {
|
return nil, fmt.Errorf("No base sprite for %v", p.Locator)
|
||||||
// FIXME: SpriteID takes precedence over SHARE if present, but is that right?
|
|
||||||
spriteId := r.Share
|
|
||||||
if len(r.SpriteId) > 0 && r.SpriteId[0] != -1 {
|
|
||||||
spriteId = r.SpriteId[0]
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sprite, err := d.menu.Sprite(r.ObjectIdx, spriteId)
|
sprite, err := d.menu.Sprite(p.ObjectIdx, spriteId)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
ni := &noninteractive{
|
ni := &noninteractive{
|
||||||
path: r.Path(),
|
locator: p.Locator,
|
||||||
frames: animation{sprite.Image},
|
frames: animation{sprite.Image},
|
||||||
hoverImpl: hoverImpl{text: r.Text},
|
rect: sprite.Rect,
|
||||||
rect: sprite.Rect,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
d.hoverables = append(d.hoverables, ni)
|
|
||||||
d.paintables = append(d.paintables, ni)
|
|
||||||
|
|
||||||
return ni, nil
|
return ni, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerHypertext(d *Driver, r *menus.Record) error {
|
func (d *Driver) buildStatic(p *menus.Properties) (*noninteractive, *Widget, error) {
|
||||||
sprite, err := d.menu.Sprite(r.ObjectIdx, r.Share)
|
ni, err := d.buildNoninteractive(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
ni := &noninteractive{
|
ni.hoverImpl.text = p.Text
|
||||||
path: r.Path(),
|
|
||||||
hoverImpl: hoverImpl{text: r.Text},
|
widget := &Widget{
|
||||||
rect: sprite.Rect,
|
Locator: ni.locator,
|
||||||
|
ownHoverables: []hoverable{ni},
|
||||||
|
ownPaintables: []paintable{ni},
|
||||||
}
|
}
|
||||||
|
|
||||||
d.clickables = append(d.clickables, ni)
|
return ni, widget, nil
|
||||||
d.hoverables = append(d.hoverables, ni)
|
}
|
||||||
|
|
||||||
return 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,
|
||||||
|
ownClickables: []clickable{ni},
|
||||||
|
ownHoverables: []hoverable{ni},
|
||||||
|
}
|
||||||
|
|
||||||
|
return ni, widget, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// An overlay is a static image + some text that needs to be rendered
|
// An overlay is a static image + some text that needs to be rendered
|
||||||
func registerOverlay(d *Driver, r *menus.Record) error {
|
func (d *Driver) buildOverlay(p *menus.Properties) (*noninteractive, *Widget, error) {
|
||||||
sprite, err := d.menu.Sprite(r.ObjectIdx, r.Share)
|
ni, err := d.buildNoninteractive(p)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
ni := &noninteractive{
|
widget := &Widget{
|
||||||
path: r.Path(),
|
Locator: ni.locator,
|
||||||
frames: animation{sprite.Image},
|
ownPaintables: []paintable{ni},
|
||||||
rect: sprite.Rect,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
d.paintables = append(d.paintables, ni)
|
if p.Text != "" {
|
||||||
|
|
||||||
if r.Text != "" {
|
|
||||||
// FIXME: is this always right? Seems to make sense for Main.mnu
|
// FIXME: is this always right? Seems to make sense for Main.mnu
|
||||||
fnt := d.menu.Font(r.FontType/10 - 1)
|
fnt := d.menu.Font(p.FontType/10 - 1)
|
||||||
|
|
||||||
ni.label = &label{
|
ni.label = &label{
|
||||||
font: fnt,
|
font: fnt,
|
||||||
rect: ni.rect, // We will be centered by default
|
rect: ni.rect, // We will be centered by default
|
||||||
text: r.Text,
|
text: p.Text,
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.Printf("Overlay without text detected: %#+v", r)
|
log.Printf("Overlay without text detected in %v", p.Locator)
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
return ni, widget, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// An animation is a non-interactive element that displays something in a loop
|
// An animation is a non-interactive element that displays something in a loop
|
||||||
func registerAnimation(d *Driver, r *menus.Record) error {
|
func (d *Driver) buildAnimationSample(p *menus.Properties) (*noninteractive, *Widget, error) {
|
||||||
sprite, err := d.menu.Sprite(r.ObjectIdx, r.SpriteId[0])
|
sprite, err := d.menu.Sprite(p.ObjectIdx, p.SpriteId[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
frames, err := d.menu.Images(r.ObjectIdx, r.SpriteId[0], r.DrawType)
|
frames, err := d.menu.Images(p.ObjectIdx, p.SpriteId[0], p.DrawType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
ani := &noninteractive{
|
ani := &noninteractive{
|
||||||
path: r.Path(),
|
locator: p.Locator,
|
||||||
frames: animation(frames),
|
frames: animation(frames),
|
||||||
hoverImpl: hoverImpl{text: r.Text},
|
hoverImpl: hoverImpl{text: p.Text},
|
||||||
rect: sprite.Rect,
|
rect: sprite.Rect,
|
||||||
}
|
}
|
||||||
|
|
||||||
d.hoverables = append(d.hoverables, ani)
|
widget := &Widget{
|
||||||
d.paintables = append(d.paintables, ani)
|
ownHoverables: []hoverable{ani},
|
||||||
|
ownPaintables: []paintable{ani},
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return ani, widget, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerAnimationHover(d *Driver, r *menus.Record) error {
|
func (d *Driver) buildAnimationHover(p *menus.Properties) (*animationHover, *Widget, error) {
|
||||||
sprite, err := d.menu.Sprite(r.ObjectIdx, r.SpriteId[0])
|
sprite, err := d.menu.Sprite(p.ObjectIdx, p.SpriteId[0])
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
enterFrames, err := d.menu.Images(r.ObjectIdx, r.SpriteId[0], r.DrawType)
|
enterFrames, err := d.menu.Images(p.ObjectIdx, p.SpriteId[0], p.DrawType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
exitFrames, err := d.menu.Images(r.ObjectIdx, r.SpriteId[0]+r.DrawType, r.DrawType)
|
exitFrames, err := d.menu.Images(p.ObjectIdx, p.SpriteId[0]+p.DrawType, p.DrawType)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
ani := &animationHover{
|
ani := &animationHover{
|
||||||
noninteractive: noninteractive{
|
noninteractive: noninteractive{
|
||||||
path: r.Path(),
|
locator: p.Locator,
|
||||||
frames: animation(enterFrames),
|
frames: animation(enterFrames),
|
||||||
hoverImpl: hoverImpl{text: r.Text},
|
hoverImpl: hoverImpl{text: p.Text},
|
||||||
rect: sprite.Rect,
|
rect: sprite.Rect,
|
||||||
},
|
},
|
||||||
|
|
||||||
exitFrames: animation(exitFrames),
|
exitFrames: animation(exitFrames),
|
||||||
}
|
}
|
||||||
|
|
||||||
d.hoverables = append(d.hoverables, ani)
|
widget := &Widget{
|
||||||
d.paintables = append(d.paintables, ani)
|
ownHoverables: []hoverable{ani},
|
||||||
|
ownPaintables: []paintable{ani},
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return ani, widget, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *noninteractive) id() string {
|
func (n *noninteractive) id() string {
|
||||||
return n.path
|
return n.locator
|
||||||
}
|
}
|
||||||
|
|
||||||
func (n *noninteractive) bounds() image.Rectangle {
|
func (n *noninteractive) bounds() image.Rectangle {
|
||||||
|
@@ -9,11 +9,6 @@ import (
|
|||||||
"code.ur.gs/lupine/ordoor/internal/menus"
|
"code.ur.gs/lupine/ordoor/internal/menus"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
|
||||||
registerBuilder(menus.TypeCheckbox, noChildren(registerCheckbox))
|
|
||||||
registerBuilder(menus.TypeSlider, noChildren(registerSlider))
|
|
||||||
}
|
|
||||||
|
|
||||||
// A checkbox can be a fancy button
|
// A checkbox can be a fancy button
|
||||||
type checkbox struct {
|
type checkbox struct {
|
||||||
button
|
button
|
||||||
@@ -23,7 +18,7 @@ type checkbox struct {
|
|||||||
|
|
||||||
// A slider is harder. Two separate elements to render
|
// A slider is harder. Two separate elements to render
|
||||||
type slider struct {
|
type slider struct {
|
||||||
path string
|
locator string
|
||||||
|
|
||||||
baseSpr *assetstore.Sprite
|
baseSpr *assetstore.Sprite
|
||||||
clickSpr *assetstore.Sprite
|
clickSpr *assetstore.Sprite
|
||||||
@@ -38,52 +33,56 @@ type slider struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// A checkbox has 3 sprites, and 3 states: unchecked, checked, disabled.
|
// A checkbox has 3 sprites, and 3 states: unchecked, checked, disabled.
|
||||||
func registerCheckbox(d *Driver, r *menus.Record) error {
|
func (d *Driver) buildCheckbox(p *menus.Properties) (*checkbox, *Widget, error) {
|
||||||
sprites, err := d.menu.Sprites(r.ObjectIdx, r.Share, 3) // unchecked, disabled, checked
|
sprites, err := d.menu.Sprites(p.ObjectIdx, p.Share, 3) // unchecked, disabled, checked
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
checkbox := &checkbox{
|
checkbox := &checkbox{
|
||||||
button: button{
|
button: button{
|
||||||
path: r.Path(),
|
locator: p.Locator,
|
||||||
baseSpr: sprites[0], // unchecked
|
baseSpr: sprites[0], // unchecked
|
||||||
clickSpr: sprites[2], // checked
|
clickSpr: sprites[2], // checked
|
||||||
frozenSpr: sprites[1],
|
frozenSpr: sprites[1], // disabled
|
||||||
hoverImpl: hoverImpl{text: r.Text},
|
hoverImpl: hoverImpl{text: p.Text},
|
||||||
},
|
},
|
||||||
valueImpl: valueImpl{str: "0"},
|
valueImpl: valueImpl{str: "0"},
|
||||||
}
|
}
|
||||||
|
|
||||||
d.clickables = append(d.clickables, checkbox)
|
widget := &Widget{
|
||||||
d.freezables = append(d.freezables, checkbox)
|
ownClickables: []clickable{checkbox},
|
||||||
d.hoverables = append(d.hoverables, checkbox)
|
ownFreezables: []freezable{checkbox},
|
||||||
d.paintables = append(d.paintables, checkbox)
|
ownHoverables: []hoverable{checkbox},
|
||||||
d.valueables = append(d.valueables, checkbox)
|
ownPaintables: []paintable{checkbox},
|
||||||
|
ownValueables: []valueable{checkbox},
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return checkbox, widget, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func registerSlider(d *Driver, r *menus.Record) error {
|
func (d *Driver) buildSlider(p *menus.Properties) (*slider, *Widget, error) {
|
||||||
sprites, err := d.menu.Sprites(r.ObjectIdx, r.Share, 3) // base, clicked, slider element
|
sprites, err := d.menu.Sprites(p.ObjectIdx, p.Share, 3) // base, clicked, slider element
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
slider := &slider{
|
slider := &slider{
|
||||||
path: r.Path(),
|
locator: p.Locator,
|
||||||
baseSpr: sprites[0],
|
baseSpr: sprites[0],
|
||||||
clickSpr: sprites[1],
|
clickSpr: sprites[1],
|
||||||
sliderSpr: sprites[2],
|
sliderSpr: sprites[2],
|
||||||
hv: sprites[0].Rect.Dy() > sprites[0].Rect.Dx(), // A best guess
|
hv: sprites[0].Rect.Dy() > sprites[0].Rect.Dx(), // A best guess
|
||||||
}
|
}
|
||||||
|
|
||||||
d.clickables = append(d.clickables, slider)
|
widget := &Widget{
|
||||||
d.mouseables = append(d.mouseables, slider)
|
ownClickables: []clickable{slider},
|
||||||
d.paintables = append(d.paintables, slider)
|
ownMouseables: []mouseable{slider},
|
||||||
d.valueables = append(d.valueables, slider)
|
ownPaintables: []paintable{slider},
|
||||||
|
ownValueables: []valueable{slider},
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
return slider, widget, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *checkbox) registerMouseClick() {
|
func (c *checkbox) registerMouseClick() {
|
||||||
@@ -107,7 +106,7 @@ func (c *checkbox) regions(tick int) []region {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *slider) id() string {
|
func (s *slider) id() string {
|
||||||
return s.path
|
return s.locator
|
||||||
}
|
}
|
||||||
|
|
||||||
// The bounds of the slider are the whole thing
|
// The bounds of the slider are the whole thing
|
||||||
|
119
internal/ui/value.go
Normal file
119
internal/ui/value.go
Normal file
@@ -0,0 +1,119 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strconv"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (d *Driver) realId(id string) string {
|
||||||
|
return fmt.Sprintf("%v:%v", d.menu.Name, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) Value(id string, into *string) error {
|
||||||
|
for _, valueable := range d.valueables() {
|
||||||
|
if valueable.id() == d.realId(id) {
|
||||||
|
*into = valueable.value()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("Couldn't find valueable widget %v:%v", d.menu.Name, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) SetValue(id, value string) error {
|
||||||
|
for _, valueable := range d.valueables() {
|
||||||
|
if valueable.id() == d.realId(id) {
|
||||||
|
valueable.setValue(value)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("Couldn't find valueable widget %v:%v", d.menu.Name, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) ValueBool(id string, into *bool) error {
|
||||||
|
var vStr string
|
||||||
|
if err := d.Value(id, &vStr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*into = vStr == "1"
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) SetValueBool(id string, value bool) error {
|
||||||
|
vStr := "0"
|
||||||
|
if value {
|
||||||
|
vStr = "1"
|
||||||
|
}
|
||||||
|
|
||||||
|
return d.SetValue(id, vStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) SetFreeze(id string, value bool) error {
|
||||||
|
for _, freezable := range d.freezables() {
|
||||||
|
if freezable.id() == d.realId(id) {
|
||||||
|
freezable.setFreezeState(value)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("Couldn't find clickable widget %v:%v", d.menu.Name, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) OnClick(id string, f func()) error {
|
||||||
|
for _, clickable := range d.clickables() {
|
||||||
|
if clickable.id() == d.realId(id) {
|
||||||
|
clickable.onClick(f)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// We need to be able to wire up items inside dialogues too
|
||||||
|
for _, dialogue := range d.dialogues {
|
||||||
|
for _, clickable := range dialogue.clickables() {
|
||||||
|
if clickable.id() == d.realId(id) {
|
||||||
|
clickable.onClick(f)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("Couldn't find clickable widget %v:%v", d.menu.Name, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// FIXME: HURK. Surely I'm missing something? steps is value:offset
|
||||||
|
func (d *Driver) ConfigureSlider(id string, steps map[int]int) error {
|
||||||
|
|
||||||
|
for _, clickable := range d.clickables() {
|
||||||
|
if slider, ok := clickable.(*slider); ok && slider.id() == d.realId(id) {
|
||||||
|
slider.steps = steps
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return fmt.Errorf("Couldn't find slider %v:%v", d.menu.Name, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) ValueInt(id string, into *int) error {
|
||||||
|
var vStr string
|
||||||
|
if err := d.Value(id, &vStr); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
value, err := strconv.Atoi(vStr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
*into = value
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (d *Driver) SetValueInt(id string, value int) error {
|
||||||
|
vStr := strconv.Itoa(value)
|
||||||
|
|
||||||
|
return d.SetValue(id, vStr)
|
||||||
|
}
|
73
internal/ui/widget.go
Normal file
73
internal/ui/widget.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
package ui
|
||||||
|
|
||||||
|
type Widget struct {
|
||||||
|
Locator string
|
||||||
|
Children []*Widget
|
||||||
|
|
||||||
|
ownClickables []clickable
|
||||||
|
ownFreezables []freezable
|
||||||
|
ownHoverables []hoverable
|
||||||
|
ownMouseables []mouseable
|
||||||
|
ownPaintables []paintable
|
||||||
|
ownValueables []valueable
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Widget) clickables() []clickable {
|
||||||
|
out := w.ownClickables
|
||||||
|
|
||||||
|
for _, widget := range w.Children {
|
||||||
|
out = append(out, widget.clickables()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Widget) freezables() []freezable {
|
||||||
|
out := w.ownFreezables
|
||||||
|
|
||||||
|
for _, widget := range w.Children {
|
||||||
|
out = append(out, widget.freezables()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Widget) hoverables() []hoverable {
|
||||||
|
out := w.ownHoverables
|
||||||
|
|
||||||
|
for _, widget := range w.Children {
|
||||||
|
out = append(out, widget.hoverables()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Widget) mouseables() []mouseable {
|
||||||
|
out := w.ownMouseables
|
||||||
|
|
||||||
|
for _, widget := range w.Children {
|
||||||
|
out = append(out, widget.mouseables()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Widget) paintables() []paintable {
|
||||||
|
out := w.ownPaintables
|
||||||
|
|
||||||
|
for _, widget := range w.Children {
|
||||||
|
out = append(out, widget.paintables()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *Widget) valueables() []valueable {
|
||||||
|
out := w.ownValueables
|
||||||
|
|
||||||
|
for _, widget := range w.Children {
|
||||||
|
out = append(out, widget.valueables()...)
|
||||||
|
}
|
||||||
|
|
||||||
|
return out
|
||||||
|
}
|
Reference in New Issue
Block a user