Compare commits

...

4 Commits

Author SHA1 Message Date
971b3178d6 Implement the options menu, part 1
This commit implements loading and saving options from/to config, and
enough UI toolkit magic to allow changes to boolean options to be
persisted.

We now respect the "play movies" setting and take screen resolution
from the config file.
2020-03-22 22:12:59 +00:00
0adbfaa573 Remove the -win-x and -win-y options for the ordoor binary 2020-03-22 22:12:20 +00:00
cfa56a0e12 Implement the main menu for the ordoor binary
In this commit, we also remove code that doesn't properly belong in
view-menu
2020-03-22 19:12:44 +00:00
d4d8a50ce4 Catch one more use of WH40K 2020-03-22 17:56:24 +00:00
16 changed files with 705 additions and 157 deletions

View File

@@ -17,6 +17,9 @@ import (
var (
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation")
gameMap = flag.String("map", "", "Name of a map, e.g., Chapter01")
winX = flag.Int("win-x", 1280, "Pre-scaled window X dimension")
winY = flag.Int("win-y", 1024, "Pre-scaled window Y dimension")
)
type env struct {
@@ -69,7 +72,7 @@ func main() {
lastState: state,
}
win, err := ui.NewWindow(env, "View Map "+*gameMap)
win, err := ui.NewWindow(env, "View Map "+*gameMap, *winX, *winY)
if err != nil {
log.Fatal("Couldn't create window: %v", err)
}

View File

@@ -7,28 +7,16 @@ import (
"code.ur.gs/lupine/ordoor/internal/assetstore"
"code.ur.gs/lupine/ordoor/internal/ui"
"github.com/hajimehoshi/ebiten"
"github.com/hajimehoshi/ebiten/audio"
)
var (
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation")
menuName = flag.String("menu", "", "Name of a menu, e.g. Main")
winX = flag.Int("win-x", 1280, "Pre-scaled window X dimension")
winY = flag.Int("win-y", 1024, "Pre-scaled window Y dimension")
)
type env struct {
ui *ui.Interface
// fonts []*assetstore.Font
// fontObjs []*assetstore.Object
step int
state state
lastState state
}
type state struct{}
func main() {
flag.Parse()
@@ -47,51 +35,12 @@ func main() {
log.Fatalf("Couldn't load menu %s: %v", *menuName, err)
}
// loadedFonts, err := loadFonts(menu.FontNames...)
// if err != nil {
// log.Fatalf("Failed to load font: %v", err)
// }
iface, err := ui.NewInterface(menu)
if err != nil {
log.Fatalf("Couldn't initialize interface: %v", err)
}
if menu.Name == "main" {
log.Printf("Installing a click handler!")
widget, err := iface.Widget(2, 5) // Menu 2, submenu 5
if err != nil {
log.Fatalf("Couldn't find widget 2,5: %v", err)
}
widget.OnMouseClick = func() {
os.Exit(0)
}
}
// Yay sound
if _, err := audio.NewContext(48000); err != nil {
log.Fatalf("Failed to audio: %v", err)
}
music, err := assets.Sound("music_interface") // FIXME: should be a reference to Sounds.dat
if err != nil {
log.Fatalf("Failed to find interface music: %v", err)
}
player, err := music.InfinitePlayer()
if err != nil {
log.Fatalf("Failed to generate music player for interface: %v", err)
}
player.Play()
state := state{}
env := &env{
ui: iface,
//objects: menuObjs,
// fonts: loadedFonts,
state: state,
lastState: state,
}
win, err := ui.NewWindow(env, "View Menu: "+*menuName)
win, err := ui.NewWindow(iface, "View Menu: "+*menuName, *winX, *winY)
if err != nil {
log.Fatal("Couldn't create window: %v", err)
}
@@ -100,14 +49,3 @@ func main() {
log.Fatal(err)
}
}
func (e *env) Update(screenX, screenY int) error {
e.step += 1
e.lastState = e.state
return e.ui.Update(screenX, screenY)
}
func (e *env) Draw(screen *ebiten.Image) error {
return e.ui.Draw(screen)
}

View File

@@ -21,6 +21,9 @@ var (
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation")
mapFile = flag.String("map", "", "Prefix path to a .map file, e.g. ./orig/Maps/Chapter01.MAP")
txtFile = flag.String("txt", "", "Prefix path to a .txt file, e.g. ./orig/Maps/Chapter01.txt")
winX = flag.Int("win-x", 1280, "Pre-scaled window X dimension")
winY = flag.Int("win-y", 1024, "Pre-scaled window Y dimension")
)
type env struct {
@@ -71,7 +74,7 @@ func main() {
}
env := &env{gameMap: gameMap, set: mapSet, state: state, lastState: state}
win, err := ui.NewWindow(env, "View Map "+*mapFile)
win, err := ui.NewWindow(env, "View Map "+*mapFile, *winX, *winY)
if err != nil {
log.Fatal("Couldn't create window: %v", err)
}

View File

@@ -17,6 +17,9 @@ var (
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation")
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")
winX = flag.Int("win-x", 1280, "Pre-scaled window X dimension")
winY = flag.Int("win-y", 1024, "Pre-scaled window Y dimension")
)
type env struct {
@@ -68,7 +71,7 @@ func main() {
lastState: state,
}
win, err := ui.NewWindow(env, "View Object: "+*objName)
win, err := ui.NewWindow(env, "View Object: "+*objName, *winX, *winY)
if err != nil {
log.Fatal(err)
}

View File

@@ -16,6 +16,9 @@ import (
var (
gamePath = flag.String("game-path", "./orig", "Path to a WH40K: Chaos Gate installation")
setName = flag.String("set", "", "Name of a set, e.g., map01")
winX = flag.Int("win-x", 1280, "Pre-scaled window X dimension")
winY = flag.Int("win-y", 1024, "Pre-scaled window Y dimension")
)
type env struct {
@@ -58,7 +61,7 @@ func main() {
lastState: state,
}
win, err := ui.NewWindow(env, "View Set: "+*setName)
win, err := ui.NewWindow(env, "View Set: "+*setName, *winX, *winY)
if err != nil {
log.Fatal("Couldn't create window: %v", err)
}

View File

@@ -1,7 +1,19 @@
[ordoor]
data_dir = "./orig"
video_player = [
"mpv",
"--no-config", "--keep-open=no", "--force-window=no", "--no-border",
"--no-osc", "--fullscreen", "--no-input-default-bindings"
]
data_dir = "./orig"
video_player = ["mpv", "--no-config", "--keep-open=no", "--force-window=no", "--no-border", "--no-osc", "--fullscreen", "--no-input-default-bindings"]
[options]
play_movies = true
animations = true
play_music = true
combat_voices = true
show_grid = false
show_paths = false
point_saving = false
auto_cut_level = false
x_resolution = 1280
y_resolution = 1024
music_volume = 100
sfx_volume = 100
unit_speed = 100
animation_speed = 100

View File

@@ -245,6 +245,21 @@ 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 currently-known values:
| Value | Type |
| ----- | ---------------- |
| 0 | Static image |
| 1 | Menu |
| 3 | Button |
| 50 | Invoke? Button? |
| 61 | "Overlay" |
| 70 | "Hypertext" |
| 91 | Checkbox |
| 220 | Animation sample |
| 228 | Main menu button |
| 232 | Slider |
### `ACTIVE`
There are only 4 values seen across all menus: `0`, `1`, `1,0`, `102` and `1,1`.

View File

@@ -1,18 +1,43 @@
package config
import (
"os"
"path/filepath"
"github.com/BurntSushi/toml"
)
type WH40K struct {
type Ordoor struct {
DataDir string `toml:"data_dir"`
VideoPlayer []string `toml:"video_player"`
}
// Things set
type Options struct {
PlayMovies bool `toml:"play_movies"`
Animations bool `toml:"animations"`
PlayMusic bool `toml:"play_music"`
CombatVoices bool `toml:"combat_voices"`
ShowGrid bool `toml:"show_grid"`
ShowPaths bool `toml:"show_paths"`
PointSaving bool `toml:"point_saving"`
AutoCutLevel bool `toml:"auto_cut_level"`
XRes int `toml:"x_resolution"`
YRes int `toml:"y_resolution"`
MusicVolume int `toml:"music_volume"`
SFXVolume int `toml:"sfx_volume"`
UnitSpeed int `toml:"unit_speed"`
AnimSpeed int `toml:"animation_speed"`
}
type Config struct {
WH40K `toml:"ordoor"`
filename string `toml:"-"`
Ordoor `toml:"ordoor"`
Options `toml:"options"`
}
func Load(filename string) (*Config, error) {
@@ -23,9 +48,21 @@ func Load(filename string) (*Config, error) {
return nil, err
}
out.filename = filename
return &out, err
}
func (c *Config) Save() error {
f, err := os.OpenFile(c.filename, os.O_WRONLY, 0644)
if err != nil {
return err
}
defer f.Close()
return toml.NewEncoder(f).Encode(c)
}
// TODO: case-insensitive lookup
func (c *Config) DataFile(path string) string {
return filepath.Join(c.DataDir, path)

View File

@@ -10,11 +10,19 @@ import (
"code.ur.gs/lupine/ordoor/internal/util/asciiscan"
)
type MenuType int
const (
TypeStatic = 0
TypeMenu = 1
TypeOverlay = 61
TypeMainButton = 228
TypeStatic MenuType = 0
TypeMenu MenuType = 1
TypeButton MenuType = 3
TypeInvokeButton MenuType = 50
TypeOverlay MenuType = 61
TypeHypertext MenuType = 70
TypeCheckbox MenuType = 91
TypeAnimationSample MenuType = 220
TypeMainButton MenuType = 228
TypeSlider MenuType = 232
)
type Record struct {
@@ -22,7 +30,7 @@ type Record struct {
Children []*Record
Id int
Type int
Type MenuType
DrawType int
FontType int
Active bool
@@ -183,7 +191,7 @@ func setProperty(r *Record, k, v string) {
case "MENUID", "SUBMENUID":
r.Id = vInt
case "MENUTYPE", "SUBMENUTYPE":
r.Type = vInt
r.Type = MenuType(vInt)
case "ACTIVE":
r.Active = (vInt != 0)
case "SPRITEID":

View File

@@ -0,0 +1,221 @@
package ordoor
import (
"fmt"
"log"
"code.ur.gs/lupine/ordoor/internal/ui"
)
func try(result error, into *error) {
if *into == nil {
*into = result
}
}
// These are UI interfaces covering the game entrypoint
func (o *Ordoor) ifaceMain() (*ui.Interface, error) {
// Start in the "main" menu
main, err := o.buildInterface("main")
if err != nil {
return nil, err
}
options, err := o.ifaceOptions(main)
if err != nil {
return nil, err
}
// TODO: clicking these buttons should load other interfaces
try(wireupClick(main, func() {}, 2, 1), &err) // New game
try(wireupClick(main, func() {}, 2, 2), &err) // Load game
try(disableWidget(main, 2, 3), &err) // Multiplayer - disable for now
try(wireupClick(main, func() { o.iface = options }, 2, 4), &err) // Options
try(wireupClick(main, func() { o.nextState = StateExit }, 2, 5), &err) // Quit
return main, err
}
// Options needs to know how to go back to main
func (o *Ordoor) ifaceOptions(main *ui.Interface) (*ui.Interface, error) {
options, err := o.buildInterface("options")
if err != nil {
return nil, err
}
if err := o.configIntoOptions(options); err != nil {
return nil, err
}
// TODO: load current options state into UI
try(wireupClick(options, func() {}, 2, 8), &err) // Keyboard settings button
// Resolution slider is 2,9
// Music volume slider is 2,10
// Sound FX volume slider is 2,11
// Accept button
try(wireupClick(
options, func() {
if err := o.optionsIntoConfig(options); err != nil {
// FIXME: exiting is a bit OTT. Perhaps display "save failed"?
log.Printf("Saving options to config failed: %v", err)
o.nextState = StateExit
} else {
o.iface = main
}
},
2, 12,
), &err)
// 13...23 are "hypertext"
// Cancel button
try(
wireupClick(
options,
func() {
// FIXME: again, exiting is OTT. We're just resetting the state of
// the interface to the values in config.
if err := o.configIntoOptions(options); err != nil {
log.Printf("Saving options to config failed: %v", err)
o.nextState = StateExit
} else {
o.iface = main
}
},
2, 24,
), &err)
// Unit speed slider is 2,26
// Looping effect speed slider is 2,27
// Sample of unit speed animation is 2,28
// Sample of effect speed animation is 2,29
// 30...35 are "hypertext"
return options, err
}
func (o *Ordoor) configIntoOptions(options *ui.Interface) error {
cfg := &o.config.Options
var err error
try(setWidgetValueBool(options, cfg.PlayMovies, 2, 1), &err)
try(setWidgetValueBool(options, cfg.Animations, 2, 2), &err)
try(setWidgetValueBool(options, cfg.PlayMusic, 2, 3), &err)
try(setWidgetValueBool(options, cfg.CombatVoices, 2, 4), &err)
try(setWidgetValueBool(options, cfg.ShowGrid, 2, 5), &err)
try(setWidgetValueBool(options, cfg.ShowPaths, 2, 6), &err)
try(setWidgetValueBool(options, cfg.PointSaving, 2, 7), &err)
try(setWidgetValueBool(options, cfg.AutoCutLevel, 2, 25), &err)
return err
}
func (o *Ordoor) optionsIntoConfig(options *ui.Interface) error {
cfg := &o.config.Options
var err error
try(getWidgetValueBool(options, &cfg.PlayMovies, 2, 1), &err)
try(getWidgetValueBool(options, &cfg.Animations, 2, 2), &err)
try(getWidgetValueBool(options, &cfg.PlayMusic, 2, 3), &err)
try(getWidgetValueBool(options, &cfg.CombatVoices, 2, 4), &err)
try(getWidgetValueBool(options, &cfg.ShowGrid, 2, 5), &err)
try(getWidgetValueBool(options, &cfg.ShowPaths, 2, 6), &err)
try(getWidgetValueBool(options, &cfg.PointSaving, 2, 7), &err)
try(getWidgetValueBool(options, &cfg.AutoCutLevel, 2, 25), &err)
if err != nil {
return err
}
return o.config.Save()
}
func (o *Ordoor) buildInterface(name string) (*ui.Interface, error) {
menu, err := o.assets.Menu(name)
if err != nil {
return nil, err
}
iface, err := ui.NewInterface(menu)
if err != nil {
return nil, err
}
return iface, nil
}
func findWidget(iface *ui.Interface, spec ...int) (*ui.Widget, error) {
widget, err := iface.Widget(spec...)
if err != nil {
return nil, fmt.Errorf("Couldn't find widget %v:%+v", iface.Name, spec)
}
return widget, nil
}
func getWidgetValue(iface *ui.Interface, spec ...int) (string, error) {
widget, err := findWidget(iface, spec...)
if err != nil {
return "", err
}
return widget.Value, nil
}
func setWidgetValue(iface *ui.Interface, value string, spec ...int) error {
widget, err := findWidget(iface, spec...)
if err != nil {
return err
}
widget.Value = value
return nil
}
func getWidgetValueBool(iface *ui.Interface, into *bool, spec ...int) error {
vStr, err := getWidgetValue(iface, spec...)
if err != nil {
return err
}
*into = vStr == "1"
return nil
}
func setWidgetValueBool(iface *ui.Interface, value bool, spec ...int) error {
vStr := "0"
if value {
vStr = "1"
}
return setWidgetValue(iface, vStr, spec...)
}
func wireupClick(iface *ui.Interface, f func(), spec ...int) error {
widget, err := findWidget(iface, spec...)
if err != nil {
return err
}
if widget.OnMouseClick != nil {
return fmt.Errorf("Widget %#+v already has an OnMouseClick handler", widget)
}
widget.OnMouseClick = f
return nil
}
func disableWidget(iface *ui.Interface, spec ...int) error {
widget, err := findWidget(iface, spec...)
if err != nil {
return err
}
widget.Disable()
return nil
}

View File

@@ -5,13 +5,41 @@
package ordoor
import (
"errors"
"fmt"
"log"
"github.com/hajimehoshi/ebiten"
"github.com/hajimehoshi/ebiten/audio"
"code.ur.gs/lupine/ordoor/internal/assetstore"
"code.ur.gs/lupine/ordoor/internal/config"
"code.ur.gs/lupine/ordoor/internal/ui"
)
type gameState int
const (
StateInitial gameState = 0
StateInterface gameState = 1
StateExit gameState = 666
)
var (
errExit = errors.New("User-requested exit action")
)
type Ordoor struct {
Config *config.Config
assets *assetstore.AssetStore
config *config.Config
music *audio.Player
win *ui.Window
state gameState
nextState gameState
// Relevant to interface state
iface *ui.Interface
}
func Run(configFile string) error {
@@ -20,14 +48,121 @@ func Run(configFile string) error {
return fmt.Errorf("Couldn't load config file: %v", err)
}
ordoor := &Ordoor{
Config: cfg,
assets, err := assetstore.New(cfg.Ordoor.DataDir)
if err != nil {
return fmt.Errorf("Failed to initialize asset store: %v", err)
}
ordoor.PlaySkippableVideo("LOGOS")
ordoor.PlaySkippableVideo("movie1")
if _, err := audio.NewContext(48000); err != nil {
return fmt.Errorf("Failed to set up audio context: %v", err)
}
// TODO: load main interface
ordoor := &Ordoor{
assets: assets,
config: cfg,
state: StateInitial,
nextState: StateInterface,
}
win, err := ui.NewWindow(ordoor, "Ordoor", cfg.Options.XRes, cfg.Options.YRes)
if err != nil {
return fmt.Errorf("Failed to create window: %v", err)
}
ordoor.win = win
if err := ordoor.Run(); err != nil {
return fmt.Errorf("Run returned %v", err)
}
return nil
}
func (o *Ordoor) Run() error {
if o.config.Options.PlayMovies {
o.PlayUnskippableVideo("LOGOS")
o.PlaySkippableVideo("movie1")
}
err := o.win.Run()
if err == errExit {
log.Printf("Exit requested")
return nil
}
return err
}
// Only one music track can play at a time. This is handled at the toplevel.
// FIXME: should take references from Sounds.dat
func (o *Ordoor) PlayMusic(name string) error {
if o.music != nil {
if err := o.music.Close(); err != nil {
return fmt.Errorf("Failed to close old music: %v", err)
}
}
sound, err := o.assets.Sound(name)
if err != nil {
return fmt.Errorf("Failed to find sound %v: %v", name, err)
}
player, err := sound.InfinitePlayer()
if err != nil {
return fmt.Errorf("Failed to generate music player for %v: %v", name, err)
}
o.music = player
player.Play()
return nil
}
func (o *Ordoor) setupInterface() error {
o.PlayMusic("music_interface")
initial, err := o.ifaceMain()
if err != nil {
return err
}
o.iface = initial
return nil
}
func (o *Ordoor) Update(screenX, screenY int) error {
// Perform state transitions
if o.state != o.nextState {
log.Printf("State transition: %v -> %v", o.state, o.nextState)
switch o.nextState {
case StateInterface: // Setup, move state to interface
if err := o.setupInterface(); err != nil {
return err
}
case StateExit:
{
return errExit
}
default:
return fmt.Errorf("Unknown state transition: %v -> %v", o.state, o.nextState)
}
}
// State transition is finished, hooray
o.state = o.nextState
switch o.state {
case StateInterface:
return o.iface.Update(screenX, screenY)
default:
return fmt.Errorf("Unknown state: %v", o.state)
}
}
func (o *Ordoor) Draw(screen *ebiten.Image) error {
switch o.state {
case StateInterface:
return o.iface.Draw(screen)
default:
return fmt.Errorf("Unknown state: %v", o.state)
}
}

View File

@@ -6,15 +6,15 @@ import (
)
func (o *Ordoor) PlayVideo(name string, skippable bool) {
filename := o.Config.DataFile("SMK/" + name + ".smk")
filename := o.config.DataFile("SMK/" + name + ".smk")
if len(o.Config.VideoPlayer) == 0 {
if len(o.config.VideoPlayer) == 0 {
log.Printf("Video player not configured, skipping video %v", filename)
return
}
argc := o.Config.VideoPlayer[0]
argv := append(o.Config.VideoPlayer[1:])
argc := o.config.VideoPlayer[0]
argv := append(o.config.VideoPlayer[1:])
if skippable {
argv = append(argv, "--input-conf=skippable.mpv.conf")
}

View File

@@ -3,6 +3,7 @@ package ui
import (
"fmt"
"image"
"log"
"reflect" // For DeepEqual
"github.com/hajimehoshi/ebiten"
@@ -20,6 +21,7 @@ import (
// mind. The interface transparently scales them all to the current screen size
// to compensate.
type Interface struct {
Name string
menu *assetstore.Menu
static []*assetstore.Sprite // Static elements in the interface
ticks int
@@ -28,6 +30,7 @@ type Interface struct {
func NewInterface(menu *assetstore.Menu) (*Interface, error) {
iface := &Interface{
Name: menu.Name,
menu: menu,
}
@@ -59,6 +62,10 @@ func (i *Interface) Update(screenX, screenY int) error {
// Iterate through all widgets, update mouse state
for _, widget := range i.widgets {
if widget.disabled {
continue // No activity for disabled widgets
}
mouseIsOver := mousePos.In(widget.Bounds)
widget.hovering(mouseIsOver)
widget.mouseButton(mouseIsOver && ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft))
@@ -105,25 +112,19 @@ func (i *Interface) Draw(screen *ebiten.Image) error {
}
func (i *Interface) addRecord(record *menus.Record) error {
switch record.Type {
case menus.TypeStatic: // These are static
if sprite, err := i.menu.Sprite(record.SpriteId[0]); err != nil {
return err
} else {
i.static = append(i.static, sprite)
}
case menus.TypeMenu: // These aren't drawable and can be ignored
case menus.TypeOverlay, menus.TypeMainButton: // Widgets \o/
if widget, err := i.widgetFromRecord(record); err != nil {
return err
} else {
i.widgets = append(i.widgets, widget)
}
log.Printf("Adding record: %#+v", record)
default:
handler, ok := setupHandlers[record.Type]
if !ok {
return fmt.Errorf("ui.interface: encountered unknown menu record: %#+v", record)
}
if handler != nil {
if err := handler(i, record); err != nil {
return err
}
}
// Recursively add all children
for _, record := range record.Children {
if err := i.addRecord(record); err != nil {
@@ -159,41 +160,3 @@ func (i *Interface) getMousePos(w, h int) image.Point {
return image.Pt(int(sX), int(sY))
}
func (i *Interface) widgetFromRecord(record *menus.Record) (*Widget, error) {
// FIXME: we assume that all widgets have a share sprite, but is that true?
sprite, err := i.menu.Sprite(record.Share)
if err != nil {
return nil, err
}
var path []int
for r := record; r != nil; r = r.Parent {
path = append([]int{r.Id}, path...)
}
widget := &Widget{
Bounds: sprite.Rect,
Tooltip: record.Desc,
path: path,
record: record,
sprite: sprite,
}
switch record.Type {
case menus.TypeMainButton:
hovers, err := i.menu.Images(record.SpriteId[0], record.DrawType)
if err != nil {
return nil, err
}
widget.hoverAnimation = hovers
sprite, err := i.menu.Sprite(record.Share + 1)
if err != nil {
return nil, err
}
widget.mouseButtonDownImage = sprite.Image
}
return widget, nil
}

View File

@@ -0,0 +1,185 @@
package ui
import (
"github.com/hajimehoshi/ebiten"
"code.ur.gs/lupine/ordoor/internal/menus"
)
// Setup handlers know how to handle each type of widget
var setupHandlers = map[menus.MenuType]func(i *Interface, r *menus.Record) error {
menus.TypeStatic: handleStatic,
menus.TypeMenu: nil,
menus.TypeButton: handleButton,
menus.TypeInvokeButton: handleInvokeButton,
menus.TypeOverlay: handleStatic, // FIXME: more?
menus.TypeHypertext: nil, // FIXME: handle this
menus.TypeCheckbox: handleCheckbox,
menus.TypeAnimationSample: nil, // FIXME: handle this
menus.TypeMainButton: handleMainButton,
menus.TypeSlider: nil, // FIXME: handle this
}
func handleStatic(i *Interface, record *menus.Record) error {
spriteId := record.Share
// FIXME: SpriteID takes precedence over SHARE if present, but is that right?
if len(record.SpriteId) > 0 && record.SpriteId[0] != -1 {
spriteId = record.SpriteId[0]
}
sprite, err := i.menu.Sprite(spriteId)
if err != nil {
return err
}
i.static = append(i.static, sprite)
return nil
}
// A checkbox has 3 sprites, and 3 states: unchecked, checked, disabled.
func handleCheckbox(i *Interface, record *menus.Record) error {
widget, err := i.widgetFromRecord(record, record.Share)
if err != nil {
return err
}
unchecked := widget.sprite
disabled, err := i.menu.Sprite(record.Share + 1)
if err != nil {
return err
}
checked, err := i.menu.Sprite(record.Share + 2)
if err != nil {
return err
}
widget.Value = "0"
widget.OnMouseClick = func() {
if widget.Value == "1" { // Click disables
widget.Value = "0"
} else { // Click enables
widget.Value = "1"
}
}
widget.disabledImage = disabled.Image
widget.valueToImage = func() *ebiten.Image {
if widget.Value == "1" {
return checked.Image
}
return unchecked.Image
}
i.widgets = append(i.widgets, widget)
return nil
}
func handleButton(i *Interface, record *menus.Record) error {
spriteId := record.SpriteId[0]
widget, err := i.widgetFromRecord(record, spriteId)
if err != nil {
return err
}
pressed, err := i.menu.Sprite(spriteId + 1)
if err != nil {
return err
}
disabled, err := i.menu.Sprite(spriteId + 2)
if err != nil {
return err
}
widget.mouseButtonDownImage = pressed.Image
widget.disabledImage = disabled.Image
i.widgets = append(i.widgets, widget)
return nil
}
func handleInvokeButton(i *Interface, record *menus.Record) error {
widget, err := i.widgetFromRecord(record, record.Share)
if err != nil {
return err
}
pressed, err := i.menu.Sprite(record.Share + 1)
if err != nil {
return err
}
disabled, err := i.menu.Sprite(record.Share + 2)
if err != nil {
return err
}
widget.mouseButtonDownImage = pressed.Image
widget.disabledImage = disabled.Image
i.widgets = append(i.widgets, widget)
return nil
}
// A main button is quite complex. It has 3 main sprites and a hover animation
func handleMainButton(i *Interface, record *menus.Record) error {
widget, err := i.widgetFromRecord(record, record.Share)
if err != nil {
return err
}
pressed, err := i.menu.Sprite(record.Share + 1)
if err != nil {
return err
}
disabled, err := i.menu.Sprite(record.Share + 2)
if err != nil {
return err
}
hovers, err := i.menu.Images(record.SpriteId[0], record.DrawType)
if err != nil {
return err
}
widget.mouseButtonDownImage = pressed.Image
widget.disabledImage = disabled.Image
widget.hoverAnimation = hovers
i.widgets = append(i.widgets, widget)
return nil
}
// Widgets need a bounding box determined by a sprite. Different widgets specify
// their sprites in different attributes, so pass in the right sprite externally
func (i *Interface) widgetFromRecord(record *menus.Record, spriteId int) (*Widget, error) {
sprite, err := i.menu.Sprite(spriteId)
if err != nil {
return nil, err
}
var path []int
for r := record; r != nil; r = r.Parent {
path = append([]int{r.Id}, path...)
}
widget := &Widget{
Bounds: sprite.Rect,
Tooltip: record.Desc,
path: path,
record: record,
sprite: sprite,
}
return widget, nil
}

View File

@@ -15,6 +15,7 @@ type Widget struct {
// Position on the screen in original (i.e., unscaled) coordinates
Bounds image.Rectangle
Tooltip string
Value string // #dealwithit for bools and ints and so on :p
OnHoverEnter func()
OnHoverLeave func()
@@ -25,6 +26,9 @@ type Widget struct {
OnMouseClick func()
OnMouseUp func()
disabled bool
disabledImage *ebiten.Image
// These are expected to have the same dimensions as the Bounds
hoverAnimation []*ebiten.Image
hoverState bool
@@ -37,6 +41,15 @@ type Widget struct {
path []int
record *menus.Record
sprite *assetstore.Sprite
valueToImage func() *ebiten.Image
}
func (w *Widget) Disable() {
w.hovering(false)
w.mouseButton(false)
w.disabled = true
}
func (w *Widget) hovering(value bool) {
@@ -72,7 +85,11 @@ func (w *Widget) mouseButton(value bool) {
}
func (w *Widget) Image(aniStep int) (*ebiten.Image, error) {
if w.hoverState && w.mouseButtonState {
if w.disabled {
return w.disabledImage, nil
}
if w.mouseButtonDownImage != nil && w.hoverState && w.mouseButtonState {
return w.mouseButtonDownImage, nil
}
@@ -80,5 +97,9 @@ func (w *Widget) Image(aniStep int) (*ebiten.Image, error) {
return w.hoverAnimation[(aniStep)%len(w.hoverAnimation)], nil
}
if w.valueToImage != nil {
return w.valueToImage(), nil
}
return w.sprite.Image, nil
}

View File

@@ -19,11 +19,7 @@ type Game interface {
var (
screenScale = flag.Float64("screen-scale", 1.0, "Scale the window by this factor")
winX = flag.Int("win-x", 1280, "Pre-scaled window X dimension")
winY = flag.Int("win-y", 1024, "Pre-scaled window Y dimension")
cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`")
cpuprofile = flag.String("cpuprofile", "", "write cpu profile to `file`")
)
type Window struct {
@@ -36,12 +32,15 @@ type Window struct {
debug bool
firstRun bool
xRes int
yRes int
}
// 0,0 is the *top left* of the window
//
// ebiten assumes a single window, so only call this once...
func NewWindow(game Game, title string) (*Window, error) {
func NewWindow(game Game, title string, xRes int, yRes int) (*Window, error) {
ebiten.SetRunnableInBackground(true)
return &Window{
@@ -50,6 +49,8 @@ func NewWindow(game Game, title string) (*Window, error) {
debug: true,
firstRun: true,
game: game,
xRes: xRes,
yRes: yRes,
}, nil
}
@@ -63,7 +64,7 @@ func (w *Window) OnMouseWheel(f func(x, y float64)) {
}
func (w *Window) Layout(_, _ int) (int, int) {
return *winX, *winY
return w.xRes, w.yRes
}
func (w *Window) Update(screen *ebiten.Image) error {
@@ -120,7 +121,7 @@ func (w *Window) Run() error {
defer pprof.StopCPUProfile()
}
ebiten.SetWindowSize(int(float64(*winX)**screenScale), int(float64(*winY)**screenScale))
ebiten.SetWindowSize(int(float64(w.xRes)*(*screenScale)), int(float64(w.yRes)*(*screenScale)))
ebiten.SetWindowTitle(w.Title)
return ebiten.RunGame(w) // Native game resolution: 640x480
return ebiten.RunGame(w)
}