From 79bfab7d6b07555b07f8f5eefe4d59b40210190b Mon Sep 17 00:00:00 2001 From: Nick Thomas Date: Thu, 26 Mar 2020 23:35:34 +0000 Subject: [PATCH] We can reach the bridge \o/ --- internal/menus/menus.go | 45 +++++++++++++---- internal/ordoor/flow/flow.go | 83 +++++++++++++++---------------- internal/ordoor/flow/load_game.go | 6 +++ internal/ordoor/flow/new_game.go | 41 +++++++++++++++ internal/ordoor/flow/options.go | 1 - internal/ordoor/ordoor.go | 1 + internal/ui/driver.go | 41 +++++++++------ internal/ui/noninteractive.go | 3 ++ internal/ui/selectors.go | 78 +++++++++++++++++++++++++++++ 9 files changed, 230 insertions(+), 69 deletions(-) create mode 100644 internal/ordoor/flow/load_game.go create mode 100644 internal/ordoor/flow/new_game.go diff --git a/internal/menus/menus.go b/internal/menus/menus.go index a9cfed5..f6a362c 100644 --- a/internal/menus/menus.go +++ b/internal/menus/menus.go @@ -40,6 +40,30 @@ const ( TypeDialogue MenuType = 300 ) +// FIXME: certain elements - especially overlays - don't have a DESC specified +// in the .mnu file, but display text specified with a number in i18n. The only +// conclusion I can draw is that they're hardcoded in the binary and set from +// outside. So, do that here. +var DescOverrides = map[string]map[string]int{ + "main": { + "2.6": 50992, + }, + "newgame": { + "2.5": 50993, + }, + "levelply": { + "2.6": 50996, + }, +} + +// FIXME: Same idea with text overrides, only these aren't mentioned in the .dta +// file at all! +var TextOverrides = map[string]map[string]string{ + "main": { + "2.7": "0.1-ordoor", + }, +} + type Record struct { Menu *Menu Parent *Record @@ -56,8 +80,8 @@ type Record struct { Y int // From i18n - Text string - Help string + Text string + Help string // FIXME: turn these into first-class data properties map[string]string @@ -239,13 +263,16 @@ type Replacer interface { } func (r *Record) Internationalize(replacer Replacer) { - // FIXME: I can't see how else this would happen in the original - if r.Menu.Name == "main" { - switch r.Path() { - case "2.6": - r.properties["DESC"] = "50992" // Chaos Gate - case "2.7": - r.Text = "ordoor-0.0.0" + if overrides, ok := TextOverrides[r.Menu.Name]; ok { + if override, ok := overrides[r.Path()]; ok { + delete(r.properties, "DESC") + r.Text = override + } + } + + if overrides, ok := DescOverrides[r.Menu.Name]; ok { + if override, ok := overrides[r.Path()]; ok { + r.properties["DESC"] = strconv.Itoa(override) } } diff --git a/internal/ordoor/flow/flow.go b/internal/ordoor/flow/flow.go index debdc93..fd78ddc 100644 --- a/internal/ordoor/flow/flow.go +++ b/internal/ordoor/flow/flow.go @@ -34,6 +34,7 @@ const ( loadGame driverName = "loadGame" options driverName = "options" kbd driverName = "keyboard" + bridge driverName = "bridge" ) var ( @@ -41,6 +42,7 @@ var ( driverNames = []driverName{ main, levelPly, singles, randomMap, newGame, loadGame, options, kbd, + bridge, } // Constants used for sliders @@ -88,6 +90,20 @@ func New(assets *assetstore.AssetStore, config *config.Config) (*Flow, error) { return out, nil } +func buildDriver(assets *assetstore.AssetStore, name driverName) (*ui.Driver, error) { + menu, err := assets.Menu(string(name)) + if err != nil { + return nil, err + } + + driver, err := ui.NewDriver(menu) + if err != nil { + return nil, err + } + + return driver, nil +} + func (f *Flow) Update(screenX, screenY int) error { if f.exit != nil { return f.exit @@ -104,48 +120,19 @@ func (f *Flow) Draw(screen *ebiten.Image) error { return f.current.Draw(screen) } -func (f *Flow) setDriver(name driverName) func() { - return func() { - f.setDriverNow(name) - } -} - -func (f *Flow) setDriverNow(name driverName) { - f.current = f.drivers[name] -} - -func (f *Flow) setExit() { - f.exit = ErrExit -} - func (f *Flow) linkDrivers() { - // Main interface + // linkMain f.onClick(main, "2.1", f.setDriver(newGame)) // New game f.onClick(main, "2.2", f.setDriver(loadGame)) // Load game f.setFreeze(main, "2.3", true) // Multiplayer - disable for now f.onClick(main, "2.4", f.setDriver(options)) // Options f.onClick(main, "2.5", f.setExit) // Quit - // New game - f.onClick(newGame, "2.1", f.setDriver(levelPly)) // New campaign button - f.onClick(newGame, "2.2", f.setDriver(singles)) // Single scenario button - f.onClick(newGame, "2.3", f.setDriver(randomMap)) // Random scenario button - f.onClick(newGame, "2.4", f.setDriver(main)) // Back button - - // Load game - f.onClick(loadGame, "3.3", f.setDriver(main)) // Cancel button - - // Options + // Now link immediate children. They will link their children, and so on + f.linkNewGame() + f.linkLoadGame() + // TODO: link multiplayer f.linkOptions() - - // Level of play select - f.onClick(levelPly, "2.5", f.setDriver(newGame)) // Back button - - // Single scenario setup - f.onClick(singles, "4.11", f.setDriver(newGame)) // Back button - - // Random map setup - f.onClick(randomMap, "2.19", f.setDriver(newGame)) // Back button } func (f *Flow) configureSlider(driver driverName, id string, steps map[int]int) { @@ -172,16 +159,24 @@ func (f *Flow) setFreeze(driver driverName, id string, value bool) { f.exit = f.drivers[driver].SetFreeze(id, value) } -func buildDriver(assets *assetstore.AssetStore, name driverName) (*ui.Driver, error) { - menu, err := assets.Menu(string(name)) - if err != nil { - return nil, err +func (f *Flow) setValueBool(driver driverName, id string, value bool) { + if f.exit != nil { + return } - driver, err := ui.NewDriver(menu) - if err != nil { - return nil, err - } - - return driver, nil + f.exit = f.drivers[driver].SetValueBool(id, value) +} + +func (f *Flow) setDriver(name driverName) func() { + return func() { + f.setDriverNow(name) + } +} + +func (f *Flow) setDriverNow(name driverName) { + f.current = f.drivers[name] +} + +func (f *Flow) setExit() { + f.exit = ErrExit } diff --git a/internal/ordoor/flow/load_game.go b/internal/ordoor/flow/load_game.go new file mode 100644 index 0000000..4cf3021 --- /dev/null +++ b/internal/ordoor/flow/load_game.go @@ -0,0 +1,6 @@ +package flow + +func (f *Flow) linkLoadGame() { + // Load game + f.onClick(loadGame, "3.3", f.setDriver(main)) // Cancel button +} diff --git a/internal/ordoor/flow/new_game.go b/internal/ordoor/flow/new_game.go new file mode 100644 index 0000000..1b46dc9 --- /dev/null +++ b/internal/ordoor/flow/new_game.go @@ -0,0 +1,41 @@ +package flow + +func (f *Flow) linkNewGame() { + // New game + f.onClick(newGame, "2.1", f.setDriver(levelPly)) // New campaign button + f.onClick(newGame, "2.2", f.setDriver(singles)) // Single scenario button + f.onClick(newGame, "2.3", f.setDriver(randomMap)) // Random scenario button + f.onClick(newGame, "2.4", f.setDriver(main)) // Back button + + f.linkLevelPly() + f.linkSingles() + f.linkRandomMap() +} + +func (f *Flow) linkLevelPly() { + // We want the default difficulty level to be Veteran, not Hero. + // FIXME: Make the radio button respect changes via setValue + resetLevel := func() { + f.setValueBool(levelPly, "2.1", false) + f.setValueBool(levelPly, "2.2", true) + } + resetLevel() + + f.onClick(levelPly, "2.5", func() { // Back button + resetLevel() + f.setDriverNow(newGame) + }) + + // FIXME: we should select a savegame if Mighty Here is selected here + // FIXME: we should show a movie here. Need an internal SMK player first + // FIXME: we should set up new game state here! + f.onClick(levelPly, "2.7", f.setDriver(bridge)) // Select button +} + +func (f *Flow) linkSingles() { + f.onClick(singles, "4.11", f.setDriver(newGame)) // Back button +} + +func (f *Flow) linkRandomMap() { + f.onClick(randomMap, "2.19", f.setDriver(newGame)) // Back button +} diff --git a/internal/ordoor/flow/options.go b/internal/ordoor/flow/options.go index 8a2cb0d..e146251 100644 --- a/internal/ordoor/flow/options.go +++ b/internal/ordoor/flow/options.go @@ -5,7 +5,6 @@ import ( ) func (f *Flow) linkOptions() { - // Main options f.onClick(options, "2.8", f.setDriver(kbd)) // Keyboard settings button f.configureSlider(options, "2.9", h3Slider) // Resolution slider diff --git a/internal/ordoor/ordoor.go b/internal/ordoor/ordoor.go index 0d4b6cb..5348eb3 100644 --- a/internal/ordoor/ordoor.go +++ b/internal/ordoor/ordoor.go @@ -91,6 +91,7 @@ func Run(configFile string, overrideX, overrideY int) error { } func (o *Ordoor) Run() error { + // FIXME: we're missing a screen about SSI here if o.config.Options.PlayMovies { o.PlayUnskippableVideo("LOGOS") o.PlaySkippableVideo("movie1") diff --git a/internal/ui/driver.go b/internal/ui/driver.go index 93f9399..1e29b6d 100644 --- a/internal/ui/driver.go +++ b/internal/ui/driver.go @@ -21,36 +21,35 @@ func init() { // FIXME: these need implementing // Needed for Keyboard.mnu (main -> options -> keyboard) - registerBuilder(menus.TypeLineKbd, nil) - registerBuilder(menus.TypeDialogue, nil) + registerBuilder(menus.TypeLineKbd, registerDebug("Unimplemented LineKbd", nil)) + registerBuilder(menus.TypeDialogue, registerDebug("Unimplemented Dialogue", nil)) // Needed for Arrange.mnu (???) - registerBuilder(menus.TypeSquadButton, nil) - registerBuilder(menus.TypeAnimationToo, nil) + registerBuilder(menus.TypeSquadButton, registerDebug("Unimplemented SquadButton", nil)) + registerBuilder(menus.TypeAnimationToo, registerDebug("Unimplemented AnimationToo", nil)) // Needed for Bridge.mnu - registerBuilder(menus.TypeDoorHotspot, nil) + registerBuilder(menus.TypeDoorHotspot, registerDebug("Unimplemented DoorHotspot", nil)) // Needed for Briefing.mnu - registerBuilder(menus.TypeLineBriefing, nil) + registerBuilder(menus.TypeLineBriefing, registerDebug("Unimplemented LineBriefing", nil)) // Needed for ChaEquip.mnu - registerBuilder(menus.TypeUnknown1, nil) - registerBuilder(menus.TypeThumb, nil) - registerBuilder(menus.TypeInventorySelect, nil) + registerBuilder(menus.TypeUnknown1, registerDebug("Unimplemented Unknown1", nil)) + registerBuilder(menus.TypeThumb, registerDebug("Unimplemented Thumb", nil)) // Needed for MainGameChaos.mnu - registerBuilder(menus.TypeStatusBar, nil) + registerBuilder(menus.TypeStatusBar, registerDebug("Unimplemented StatusBar", nil)) // Needed for Multiplayer_Choose.mnu - registerBuilder(menus.TypeComboBoxItem, nil) - registerBuilder(menus.TypeDropdownButton, nil) + registerBuilder(menus.TypeComboBoxItem, registerDebug("Unimplemented ComboBoxItem", nil)) + registerBuilder(menus.TypeDropdownButton, registerDebug("Unimplemented DropdownButton", nil)) // Needed for Multiplayer_Configure.mnu - registerBuilder(menus.TypeEditBox, nil) + registerBuilder(menus.TypeEditBox, registerDebug("Unimplemented EditBox", nil)) // Needed for Multiplayer_Connect.mnu - registerBuilder(menus.TypeRadioButton, nil) + registerBuilder(menus.TypeRadioButton, registerDebug("Unimplemented RadioButton", nil)) } const ( @@ -66,6 +65,19 @@ var ( // Used to add widgets to a driver type builderFunc func(d *Driver, r *menus.Record) error +func registerDebug(reason string, onward builderFunc) builderFunc { + return func(d *Driver, r *menus.Record) error { + log.Printf("%v: %#+v", reason, r) + if onward == nil { + return registerStatic(d, r) + } else { + return onward(d, r) + } + + return nil + } +} + func registerBuilder(t menus.MenuType, f builderFunc) { if _, ok := widgetBuilders[t]; ok { panic(fmt.Sprintf("A builder for menu type %v already exists", t)) @@ -187,7 +199,6 @@ 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 - log.Printf("Found slider %#+v", slider) return nil } diff --git a/internal/ui/noninteractive.go b/internal/ui/noninteractive.go index 88fda2b..3f74227 100644 --- a/internal/ui/noninteractive.go +++ b/internal/ui/noninteractive.go @@ -2,6 +2,7 @@ package ui import ( "image" + "log" "github.com/hajimehoshi/ebiten" "github.com/hajimehoshi/ebiten/ebitenutil" @@ -91,6 +92,8 @@ func registerOverlay(d *Driver, r *menus.Record) error { ebitenutil.DebugPrint(textImg, r.Text) ni.textImg = textImg + } else { + log.Printf("Overlay without text detected: %#+v", r) } d.paintables = append(d.paintables, ni) diff --git a/internal/ui/selectors.go b/internal/ui/selectors.go index 76576ff..be8d4eb 100644 --- a/internal/ui/selectors.go +++ b/internal/ui/selectors.go @@ -12,6 +12,7 @@ import ( func init() { registerBuilder(menus.TypeCheckbox, registerCheckbox) registerBuilder(menus.TypeSlider, registerSlider) + registerBuilder(menus.TypeInventorySelect, registerInventorySelect) } // A checkbox can be a fancy button @@ -37,6 +38,15 @@ type slider struct { valueImpl } +// 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 +type inventorySelect struct { + checkbox + + parentPath string + others []*inventorySelect +} + // A checkbox has 3 sprites, and 3 states: unchecked, checked, disabled. func registerCheckbox(d *Driver, r *menus.Record) error { sprites, err := d.menu.Sprites(r.Share, 3) // unchecked, disabled, checked @@ -86,6 +96,60 @@ func registerSlider(d *Driver, r *menus.Record) error { return nil } +func registerInventorySelect(d *Driver, r *menus.Record) error { + sprites, err := d.menu.Sprites(r.Share, 3) // unchecked, checked, disabled + if err != nil { + return err + } + + element := &inventorySelect{ + checkbox: checkbox{ + 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"}, + }, + } + + d.clickables = append(d.clickables, element) + d.freezables = append(d.freezables, element) + d.hoverables = append(d.hoverables, element) + d.paintables = append(d.paintables, element) + d.valueables = append(d.valueables, element) + + if r.Parent == nil { + return nil + } + + element.parentPath = r.Parent.Path() + + // Now update all inventory selects belonging to the same menu so they share + // a list of all inventory selects. This will be replaced several times as + // the menu is built. + var inventorySelects []*inventorySelect + for _, valueable := range d.valueables { + if is, ok := valueable.(*inventorySelect); ok && is.parentPath == element.parentPath { + inventorySelects = append(inventorySelects, is) + } + } + + for _, is := range inventorySelects { + is.others = inventorySelects + } + + // Select the first in the list + if len(inventorySelects) == 1 { + element.setValue("1") + } + + return nil +} + func (c *checkbox) registerMouseClick() { if c.value() == "1" { // Click disables c.setValue("0") @@ -254,3 +318,17 @@ func (s *slider) valueInt() int { func (s *slider) value() string { return strconv.Itoa(s.valueInt()) } + +func (i *inventorySelect) registerMouseClick() { + // Do nothing if we're already selected + if i.value() == "1" { + return + } + + // Turn us on, turn everyone else off + for _, other := range i.others { + other.setValue("0") + } + + i.setValue("1") +}