Compare commits
4 Commits
3cb32b8962
...
971b3178d6
Author | SHA1 | Date | |
---|---|---|---|
971b3178d6 | |||
0adbfaa573 | |||
cfa56a0e12 | |||
d4d8a50ce4 |
@@ -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)
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
|
@@ -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
|
||||
|
@@ -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`.
|
||||
|
@@ -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)
|
||||
|
@@ -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":
|
||||
|
221
internal/ordoor/interfaces.go
Normal file
221
internal/ordoor/interfaces.go
Normal 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
|
||||
}
|
@@ -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)
|
||||
}
|
||||
}
|
||||
|
@@ -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")
|
||||
}
|
||||
|
@@ -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
|
||||
}
|
||||
|
185
internal/ui/setup_handlers.go
Normal file
185
internal/ui/setup_handlers.go
Normal 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
|
||||
}
|
@@ -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
|
||||
}
|
||||
|
@@ -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)
|
||||
}
|
||||
|
Reference in New Issue
Block a user