added select and popup screens

This commit is contained in:
Thomas von Dein 2024-02-18 18:19:34 +01:00
parent 4c9ab36fa1
commit e12af87fb7
13 changed files with 379 additions and 55 deletions

13
TODO.md
View File

@ -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)
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 50 KiB

View File

@ -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(

View File

@ -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()

View File

@ -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)
}

View File

@ -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,
}

97
game/menu_scene.go Normal file
View File

@ -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(),
}
}

101
game/popup_scene.go Normal file
View File

@ -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(),
}
}

View File

@ -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)
}

View File

@ -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(),

View File

@ -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(

View File

@ -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(

BIN
src/background-popup.xcf Normal file

Binary file not shown.