diff --git a/TODO.md b/TODO.md index 3ba1245..80a120a 100644 --- a/TODO.md +++ b/TODO.md @@ -10,3 +10,16 @@ - Grid Observer: https://github.com/mlange-42/arche/issues/374 + + +Screenshot: + +Yes, since *ebiten.Image implements the standard image.Image interface +I just made a screenshot example (in Draw() function): +if inpututil.IsKeyJustPressed(ebiten.KeyS) { + f, err := os.Create("screenshot.png") + if err != nil { + log.Fatal("can't create file: ", err) + } + png.Encode(f, screen) +} diff --git a/assets/sprites/background-popup.png b/assets/sprites/background-popup.png new file mode 100644 index 0000000..4e6b95b Binary files /dev/null and b/assets/sprites/background-popup.png differ diff --git a/game/about_scene.go b/game/about_scene.go index 55889d0..c2b0909 100644 --- a/game/about_scene.go +++ b/game/about_scene.go @@ -50,6 +50,10 @@ func (scene *AboutScene) ResetNext() { scene.Next = scene.Whoami } +func (scene *AboutScene) Clearscreen() bool { + return true +} + func (scene *AboutScene) Update() error { scene.Ui.Update() return nil @@ -66,7 +70,7 @@ func (scene *AboutScene) SetupUI() { buttonBack := gameui.NewMenuButton("Back", *assets.FontRenderer.FontNormal, func(args *widget.ButtonClickedEventArgs) { - scene.SetNext(Select) + scene.SetNext(Menu) }) label := widget.NewText( diff --git a/game/game.go b/game/game.go index 5243705..0820f93 100644 --- a/game/game.go +++ b/game/game.go @@ -16,6 +16,7 @@ type Game struct { Scenes map[SceneName]Scene CurrentScene SceneName Observer *observers.GameObserver + Levels []*Level // needed to feed select_scene } func NewGame(width, height, cellsize, startlevel int, startscene SceneName) *Game { @@ -34,9 +35,12 @@ func NewGame(width, height, cellsize, startlevel int, startscene SceneName) *Gam game.Observer = observers.NewGameObserver(&world, startlevel, width, height, cellsize) game.Scenes[Welcome] = NewWelcomeScene(game) - game.Scenes[Select] = NewSelectScene(game) + game.Scenes[Menu] = NewMenuScene(game) game.Scenes[About] = NewAboutScene(game) + game.Scenes[Popup] = NewPopupScene(game) game.Scenes[Play] = NewLevelScene(game, startlevel) + game.Scenes[Select] = NewSelectScene(game) + game.CurrentScene = startscene fmt.Println(game.World.Stats().String()) @@ -50,6 +54,9 @@ func (game *Game) GetCurrentScene() Scene { func (game *Game) Update() error { gameobserver := observers.GetGameObserver(game.World) + + // handle level ends + // FIXME: add a scene here, which asks: restart, next or abort timer := gameobserver.StopTimer if timer.IsReady() { @@ -61,6 +68,12 @@ func (game *Game) Update() error { scene := game.GetCurrentScene() scene.Update() + if scene.Clearscreen() { + ebiten.SetScreenClearedEveryFrame(true) + } else { + ebiten.SetScreenClearedEveryFrame(false) + } + next := scene.GetNext() if next != game.CurrentScene { scene.ResetNext() diff --git a/game/level_scene.go b/game/level_scene.go index 418d171..8d4747e 100644 --- a/game/level_scene.go +++ b/game/level_scene.go @@ -30,6 +30,8 @@ func (scene *LevelScene) GenerateLevels(game *Game) { for _, level := range assets.Levels { scene.Levels = append(scene.Levels, NewLevel(game, 32, &level)) } + + scene.Game.Levels = scene.Levels } // Interface methods @@ -46,18 +48,28 @@ func (scene *LevelScene) ResetNext() { scene.Next = scene.Whoami } +func (scene *LevelScene) Clearscreen() bool { + return false +} + func (scene *LevelScene) Update() error { + + scene.Levels[scene.CurrentLevel].Update() + + switch { + case ebiten.IsKeyPressed(ebiten.KeyEscape): + scene.SetNext(Popup) + } + + return nil +} + +func (scene *LevelScene) Draw(screen *ebiten.Image) { if scene.CurrentLevel != scene.Game.Observer.CurrentLevel { slog.Debug("level", "current", scene.CurrentLevel, "next", scene.Game.Observer.CurrentLevel) scene.CurrentLevel = scene.Game.Observer.CurrentLevel scene.Levels[scene.CurrentLevel].SetupGrid(scene.Game) } - - scene.Levels[scene.CurrentLevel].Update() - return nil -} - -func (scene *LevelScene) Draw(screen *ebiten.Image) { screen.Clear() scene.Levels[scene.CurrentLevel].Draw(screen) } diff --git a/game/levels.go b/game/levels.go index 798a33b..4e8688d 100644 --- a/game/levels.go +++ b/game/levels.go @@ -39,6 +39,7 @@ func NewLevel(game *Game, cellsize int, plan *assets.RawLevel) *Level { Width: game.ScreenWidth, Height: game.ScreenHeight, Description: plan.Description, + Name: plan.Name, GridSystem: gridsystem, Player: playersystem, } diff --git a/game/menu_scene.go b/game/menu_scene.go new file mode 100644 index 0000000..8f95fd8 --- /dev/null +++ b/game/menu_scene.go @@ -0,0 +1,97 @@ +package game + +import ( + "image/color" + "log/slog" + "openquell/assets" + "openquell/gameui" + "os" + + "github.com/ebitenui/ebitenui" + "github.com/ebitenui/ebitenui/widget" + + "github.com/hajimehoshi/ebiten/v2" +) + +type MenuScene struct { + Game *Game + Next SceneName + Whoami SceneName + UseCache bool + Ui *ebitenui.UI +} + +func NewMenuScene(game *Game) Scene { + scene := &MenuScene{Whoami: Menu, Game: game, Next: Menu} + + scene.SetupUI() + + return scene +} + +func (scene *MenuScene) GetNext() SceneName { + return scene.Next +} + +func (scene *MenuScene) ResetNext() { + scene.Next = scene.Whoami +} + +func (scene *MenuScene) SetNext(next SceneName) { + slog.Debug("select setnext", "next", next) + scene.Next = next +} + +func (scene *MenuScene) Clearscreen() bool { + return true +} + +func (scene *MenuScene) Update() error { + scene.Ui.Update() + return nil +} + +func (scene *MenuScene) Draw(screen *ebiten.Image) { + scene.Ui.Draw(screen) +} + +func (scene *MenuScene) SetupUI() { + blue := color.RGBA{0, 255, 128, 255} + + rowContainer := gameui.NewRowContainer() + + buttonStartnew := gameui.NewMenuButton("Start new game", *assets.FontRenderer.FontNormal, + func(args *widget.ButtonClickedEventArgs) { + scene.SetNext(Play) + }) + + buttonMenuLevel := gameui.NewMenuButton("Select Level", *assets.FontRenderer.FontNormal, + func(args *widget.ButtonClickedEventArgs) { + scene.SetNext(Select) + }) + + buttonAbout := gameui.NewMenuButton("About this game", *assets.FontRenderer.FontNormal, + func(args *widget.ButtonClickedEventArgs) { + scene.SetNext(About) + }) + + buttonQuit := gameui.NewMenuButton("Quit Game", *assets.FontRenderer.FontNormal, + func(args *widget.ButtonClickedEventArgs) { + os.Exit(1) // FIXME: present another scene "are you sure and/or thank you" + }) + + label := widget.NewText( + widget.TextOpts.Text("Menu", *assets.FontRenderer.FontBig, blue), + widget.TextOpts.Position(widget.TextPositionCenter, widget.TextPositionCenter), + ) + + rowContainer.AddChild(label) + rowContainer.AddChild(buttonStartnew) + rowContainer.AddChild(buttonMenuLevel) + rowContainer.AddChild(buttonAbout) + rowContainer.AddChild(buttonQuit) + + scene.Ui = &ebitenui.UI{ + Container: rowContainer.Container(), + } +} diff --git a/game/popup_scene.go b/game/popup_scene.go new file mode 100644 index 0000000..f0476f8 --- /dev/null +++ b/game/popup_scene.go @@ -0,0 +1,101 @@ +package game + +import ( + "image/color" + "log/slog" + "openquell/assets" + "openquell/gameui" + + "github.com/ebitenui/ebitenui" + "github.com/ebitenui/ebitenui/widget" + + "github.com/hajimehoshi/ebiten/v2" +) + +type PopupScene struct { + Game *Game + Next SceneName + Whoami SceneName + UseCache bool + Ui *ebitenui.UI +} + +func NewPopupScene(game *Game) Scene { + scene := &PopupScene{Whoami: Popup, Game: game, Next: Popup} + + scene.SetupUI() + + return scene +} + +func (scene *PopupScene) GetNext() SceneName { + return scene.Next +} + +func (scene *PopupScene) ResetNext() { + scene.Next = scene.Whoami +} + +func (scene *PopupScene) SetNext(next SceneName) { + slog.Debug("select setnext", "next", next) + scene.Next = next +} + +func (scene *PopupScene) Clearscreen() bool { + // both level_scene AND the popup must not clear to get an actual popup + return false +} + +func (scene *PopupScene) Update() error { + scene.Ui.Update() + return nil +} + +func (scene *PopupScene) Draw(screen *ebiten.Image) { + background := assets.Assets["background-popup"] + + op := &ebiten.DrawImageOptions{} + op.GeoM.Translate( + float64((scene.Game.ScreenWidth/2)-(background.Bounds().Dx()/2)), + float64((scene.Game.ScreenHeight/2)-(background.Bounds().Dy()/2)), + ) + + screen.DrawImage(assets.Assets["background-popup"], op) + + scene.Ui.Draw(screen) +} + +func (scene *PopupScene) SetupUI() { + blue := color.RGBA{0, 255, 128, 255} + + rowContainer := gameui.NewRowContainer(false) + + buttonContinue := gameui.NewMenuButton("Continue", *assets.FontRenderer.FontNormal, + func(args *widget.ButtonClickedEventArgs) { + scene.SetNext(Play) + }) + + buttonAbort := gameui.NewMenuButton("Abort", *assets.FontRenderer.FontNormal, + func(args *widget.ButtonClickedEventArgs) { + scene.SetNext(Menu) + }) + + buttonOptions := gameui.NewMenuButton("Options", *assets.FontRenderer.FontNormal, + func(args *widget.ButtonClickedEventArgs) { + scene.SetNext(Settings) + }) + + label := widget.NewText( + widget.TextOpts.Text("Menu", *assets.FontRenderer.FontBig, blue), + widget.TextOpts.Position(widget.TextPositionCenter, widget.TextPositionCenter), + ) + + rowContainer.AddChild(label) + rowContainer.AddChild(buttonContinue) + rowContainer.AddChild(buttonAbort) + rowContainer.AddChild(buttonOptions) + + scene.Ui = &ebitenui.UI{ + Container: rowContainer.Container(), + } +} diff --git a/game/scene.go b/game/scene.go index 0f2c2c3..faf17fe 100644 --- a/game/scene.go +++ b/game/scene.go @@ -5,15 +5,17 @@ import ( ) const ( - Welcome = iota - Select - Play - About - Settings + Welcome = iota // startup + Menu // main top level menu + Play // actual playing happens here + About // about the game + Settings // options + Popup // in-game options + Select // select which level to play ) // Wrapper for different screens to be shown, as Welcome, Options, -// About, Select Level and of course the actual Levels. +// About, Menu Level and of course the actual Levels. // Scenes are responsible for screen clearing! That way a scene is able // to render its content onto the running level, e.g. the options scene // etc. @@ -21,6 +23,7 @@ type Scene interface { SetNext(SceneName) GetNext() SceneName ResetNext() + Clearscreen() bool Update() error Draw(screen *ebiten.Image) } diff --git a/game/select_scene.go b/game/select_scene.go index 8ffbb40..d2a2b68 100644 --- a/game/select_scene.go +++ b/game/select_scene.go @@ -1,24 +1,26 @@ package game import ( + "fmt" "image/color" - "log/slog" "openquell/assets" "openquell/gameui" - "os" + "openquell/observers" "github.com/ebitenui/ebitenui" + "github.com/ebitenui/ebitenui/image" "github.com/ebitenui/ebitenui/widget" "github.com/hajimehoshi/ebiten/v2" ) type SelectScene struct { - Game *Game - Next SceneName - Whoami SceneName - UseCache bool - Ui *ebitenui.UI + Game *Game + Next SceneName + Whoami SceneName + UseCache bool + Ui *ebitenui.UI + SelectedLevel int } func NewSelectScene(game *Game) Scene { @@ -38,10 +40,14 @@ func (scene *SelectScene) ResetNext() { } func (scene *SelectScene) SetNext(next SceneName) { - slog.Debug("select setnext", "next", next) scene.Next = next } +func (scene *SelectScene) Clearscreen() bool { + // both level_scene AND the popup must not clear to get an actual popup + return true +} + func (scene *SelectScene) Update() error { scene.Ui.Update() return nil @@ -51,41 +57,101 @@ func (scene *SelectScene) Draw(screen *ebiten.Image) { scene.Ui.Draw(screen) } +type LevelEntry struct { + Id int + Name string +} + func (scene *SelectScene) SetupUI() { + gameobserver := observers.GetGameObserver(scene.Game.World) + blue := color.RGBA{0, 255, 128, 255} rowContainer := gameui.NewRowContainer() - buttonStartnew := gameui.NewMenuButton("Start new game", *assets.FontRenderer.FontNormal, + label := widget.NewText( + widget.TextOpts.Text("Select Level", *assets.FontRenderer.FontBig, blue), + widget.TextOpts.Position(widget.TextPositionCenter, widget.TextPositionCenter), + ) + + levels := make([]any, 0, len(scene.Game.Levels)) + for id := 0; id < len(scene.Game.Levels); id++ { + levels = append(levels, LevelEntry{Id: id, Name: scene.Game.Levels[id].Name}) + } + + buttonImage, err := gameui.LoadButtonImage() + if err != nil { + panic(err) + } + + list := widget.NewList( + // Set how wide the list should be + widget.ListOpts.ContainerOpts(widget.ContainerOpts.WidgetOpts( + widget.WidgetOpts.MinSize(150, 0), + widget.WidgetOpts.LayoutData(widget.AnchorLayoutData{ + HorizontalPosition: widget.AnchorLayoutPositionCenter, + VerticalPosition: widget.AnchorLayoutPositionEnd, + StretchVertical: true, + }), + )), + // Set the entries in the list + widget.ListOpts.Entries(levels), + widget.ListOpts.ScrollContainerOpts( + // Set the background images/color for the list + widget.ScrollContainerOpts.Image(&widget.ScrollContainerImage{ + Idle: image.NewNineSliceColor(color.NRGBA{100, 100, 100, 255}), + Disabled: image.NewNineSliceColor(color.NRGBA{100, 100, 100, 255}), + Mask: image.NewNineSliceColor(color.NRGBA{100, 100, 100, 255}), + }), + ), + widget.ListOpts.SliderOpts( + // Set the background images/color for the background of the slider track + widget.SliderOpts.Images(&widget.SliderTrackImage{ + Idle: image.NewNineSliceColor(color.NRGBA{100, 100, 100, 255}), + Hover: image.NewNineSliceColor(color.NRGBA{100, 100, 100, 255}), + }, buttonImage), + widget.SliderOpts.MinHandleSize(5), + // Set how wide the track should be + widget.SliderOpts.TrackPadding(widget.NewInsetsSimple(2))), + // Hide the horizontal slider + widget.ListOpts.HideHorizontalSlider(), + // Set the font for the list options + widget.ListOpts.EntryFontFace(*assets.FontRenderer.FontNormal), + // Set the colors for the list + widget.ListOpts.EntryColor(&widget.ListEntryColor{ + Selected: color.NRGBA{0, 255, 0, 255}, // Foreground color for the unfocused selected entry + Unselected: color.NRGBA{254, 255, 255, 255}, // Foreground color for the unfocused unselected entry + SelectedBackground: color.NRGBA{R: 130, G: 130, B: 200, A: 255}, // Background color for the unfocused selected entry + SelectedFocusedBackground: color.NRGBA{R: 130, G: 130, B: 170, A: 255}, // Background color for the focused selected entry + FocusedBackground: color.NRGBA{R: 170, G: 170, B: 180, A: 255}, // Background color for the focused unselected entry + DisabledUnselected: color.NRGBA{100, 100, 100, 255}, // Foreground color for the disabled unselected entry + DisabledSelected: color.NRGBA{100, 100, 100, 255}, // Foreground color for the disabled selected entry + DisabledSelectedBackground: color.NRGBA{100, 100, 100, 255}, // Background color for the disabled selected entry + }), + // This required function returns the string displayed in the list + widget.ListOpts.EntryLabelFunc(func(e interface{}) string { + return e.(LevelEntry).Name + }), + // Padding for each entry + widget.ListOpts.EntryTextPadding(widget.NewInsetsSimple(5)), + // Text position for each entry + widget.ListOpts.EntryTextPosition(widget.TextPositionStart, widget.TextPositionCenter), + // This handler defines what function to run when a list item is selected. + widget.ListOpts.EntrySelectedHandler(func(args *widget.ListEntrySelectedEventArgs) { + entry := args.Entry.(LevelEntry) + fmt.Println("Entry Selected: ", entry) + gameobserver.CurrentLevel = entry.Id + }), + ) + + buttonPlay := gameui.NewMenuButton("Play selected level", *assets.FontRenderer.FontNormal, func(args *widget.ButtonClickedEventArgs) { scene.SetNext(Play) }) - buttonSelectLevel := gameui.NewMenuButton("Select Level", *assets.FontRenderer.FontNormal, - func(args *widget.ButtonClickedEventArgs) { - scene.SetNext(Select) - }) - - buttonAbout := gameui.NewMenuButton("About this game", *assets.FontRenderer.FontNormal, - func(args *widget.ButtonClickedEventArgs) { - scene.SetNext(About) - }) - - buttonQuit := gameui.NewMenuButton("Quit Game", *assets.FontRenderer.FontNormal, - func(args *widget.ButtonClickedEventArgs) { - os.Exit(1) // FIXME: present another scene "are you sure and/or thank you" - }) - - label := widget.NewText( - widget.TextOpts.Text("Menu", *assets.FontRenderer.FontBig, blue), - widget.TextOpts.Position(widget.TextPositionCenter, widget.TextPositionCenter), - ) - rowContainer.AddChild(label) - rowContainer.AddChild(buttonStartnew) - rowContainer.AddChild(buttonSelectLevel) - rowContainer.AddChild(buttonAbout) - rowContainer.AddChild(buttonQuit) + rowContainer.AddChild(list) + rowContainer.AddChild(buttonPlay) scene.Ui = &ebitenui.UI{ Container: rowContainer.Container(), diff --git a/game/welcome_scene.go b/game/welcome_scene.go index f6186d4..5d0c4ba 100644 --- a/game/welcome_scene.go +++ b/game/welcome_scene.go @@ -36,10 +36,14 @@ func (scene *WelcomeScene) ResetNext() { scene.Next = scene.Whoami } +func (scene *WelcomeScene) Clearscreen() bool { + return true +} + func (scene *WelcomeScene) Update() error { switch { case ebiten.IsKeyPressed(ebiten.KeyEnter): - scene.SetNext(Select) + scene.SetNext(Menu) } scene.Ui.Update() @@ -60,7 +64,7 @@ func (scene *WelcomeScene) SetupUI() { button := gameui.NewMenuButton("Start", *assets.FontRenderer.FontNormal, func(args *widget.ButtonClickedEventArgs) { - scene.SetNext(Select) + scene.SetNext(Menu) }) label := widget.NewText( diff --git a/gameui/widgets.go b/gameui/widgets.go index 2e71674..f4dab98 100644 --- a/gameui/widgets.go +++ b/gameui/widgets.go @@ -52,14 +52,24 @@ func (container *RowContainer) Container() *widget.Container { return container.Root } -func NewRowContainer() *RowContainer { +// set arg to false if no background needed +func NewRowContainer(setbackground ...bool) *RowContainer { background := assets.Assets["background-lila"] + var uiContainer *widget.Container - uiContainer := widget.NewContainer( - widget.ContainerOpts.BackgroundImage( - image.NewNineSlice(background, [3]int{0, 1, 639}, [3]int{0, 1, 479})), - widget.ContainerOpts.Layout(widget.NewAnchorLayout()), - ) + if len(setbackground) > 0 { + // false + uiContainer = widget.NewContainer( + widget.ContainerOpts.Layout(widget.NewAnchorLayout()), + ) + } else { + // default: true + uiContainer = widget.NewContainer( + widget.ContainerOpts.BackgroundImage( + image.NewNineSlice(background, [3]int{0, 1, 639}, [3]int{0, 1, 479})), + widget.ContainerOpts.Layout(widget.NewAnchorLayout()), + ) + } rowContainer := widget.NewContainer( widget.ContainerOpts.WidgetOpts( diff --git a/src/background-popup.xcf b/src/background-popup.xcf new file mode 100644 index 0000000..3ee6821 Binary files /dev/null and b/src/background-popup.xcf differ