added intermediate popup after win/loose, refresh level setup, fixes

This commit is contained in:
Thomas von Dein 2024-02-19 19:05:48 +01:00
parent e12af87fb7
commit fd570216f0
16 changed files with 322 additions and 20 deletions

View File

@ -23,3 +23,7 @@ if inpututil.IsKeyJustPressed(ebiten.KeyS) {
}
png.Encode(f, screen)
}
- Add some final message when the player reaches the last level, start
from scratch or a message to buy me some beer, whatever

View File

@ -5,13 +5,13 @@ Background: background-lila
########
#o o #o#
# o # #
# S # #
# #### #
# #### #
# #
#o o#
# #
########

View File

@ -1,5 +1,5 @@
Description: find the fruit
Background: background-orange
Background: background-lila
# ############
@ -7,7 +7,7 @@ Background: background-orange
############ #
# o S #
# ######## ###
#
+ #
############ #
# o # #
# # ######## #

View File

@ -28,6 +28,7 @@ type Tile struct {
Renderable bool
Velocity bool
Collectible bool
Obstacle bool
Particle int // -1=unused, 0-3 = show image of slice
Particles []*ebiten.Image
}
@ -64,6 +65,17 @@ func NewTileCollectible(class string) *Tile {
}
}
func NewTileObstacle(class string) *Tile {
return &Tile{
Id: '+',
Sprite: Assets[class],
Class: class,
Solid: false,
Renderable: true,
Obstacle: true,
}
}
func NewTileParticle(class []string) *Tile {
sprites := []*ebiten.Image{}
@ -109,6 +121,7 @@ func InitTiles() TileRegistry {
'#': NewTileBlock("block-grey32"),
'S': NewTilePlayer(),
'o': NewTileCollectible("collectible-orange"),
'+': NewTileObstacle("obstacle-star"),
'*': NewTileParticle([]string{
//"particle-ring-1",
"particle-ring-2",

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.3 KiB

View File

@ -26,3 +26,4 @@ type Solid struct{}
type Floor struct{}
type Player struct{}
type Collectible struct{}
type Obstacle struct{}

View File

@ -3,6 +3,7 @@ package game
import (
"fmt"
"image"
"log/slog"
"openquell/observers"
"github.com/hajimehoshi/ebiten/v2"
@ -38,7 +39,7 @@ func NewGame(width, height, cellsize, startlevel int, startscene SceneName) *Gam
game.Scenes[Menu] = NewMenuScene(game)
game.Scenes[About] = NewAboutScene(game)
game.Scenes[Popup] = NewPopupScene(game)
game.Scenes[Play] = NewLevelScene(game, startlevel)
//game.Scenes[Play] = NewLevelScene(game, startlevel)
game.Scenes[Select] = NewSelectScene(game)
game.CurrentScene = startscene
@ -56,13 +57,20 @@ 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() {
// a level is either lost or won, we display a small popup
// asking the user how to continue from here
timer.Reset()
gameobserver.CurrentLevel++
gameobserver.Score++ // FIXME: use level.Score(), see TODO
slog.Debug("timer ready", "lost", gameobserver.Lost, "retry", gameobserver.Retry)
if !gameobserver.Lost {
gameobserver.Score++ // FIXME: use level.Score(), see TODO
}
game.Scenes[Nextlevel] = NewNextlevelScene(game, gameobserver.Lost)
game.CurrentScene = Nextlevel
}
scene := game.GetCurrentScene()
@ -76,8 +84,27 @@ func (game *Game) Update() error {
next := scene.GetNext()
if next != game.CurrentScene {
if next == Play && game.CurrentScene == Nextlevel {
// switched from nextlevel (lost or won) popup to play (either retry or next level)
if !gameobserver.Retry {
gameobserver.CurrentLevel++
}
gameobserver.Retry = false
}
if next == Play {
// fresh setup of actual level every time we enter the play scene
game.Scenes[Play] = NewLevelScene(game, gameobserver.CurrentLevel)
}
// make sure we stay on the selected scene
scene.ResetNext()
// finally switch
game.CurrentScene = next
// FIXME: add some reset function to gameobserver for these kinds of things
gameobserver.Lost = false
}
timer.Update()

View File

@ -30,6 +30,7 @@ type Level struct {
func NewLevel(game *Game, cellsize int, plan *assets.RawLevel) *Level {
gridsystem := systems.NewGridSystem(game.World, game.ScreenWidth,
game.ScreenHeight, cellsize, plan.Background)
playersystem := systems.NewPlayerSystem(game.World, gridsystem)
return &Level{

117
game/nextlevel_scene.go Normal file
View File

@ -0,0 +1,117 @@
package game
import (
"image/color"
"log/slog"
"openquell/assets"
"openquell/gameui"
"openquell/observers"
"github.com/ebitenui/ebitenui"
"github.com/ebitenui/ebitenui/widget"
"github.com/hajimehoshi/ebiten/v2"
)
type NextlevelScene struct {
Game *Game
Next SceneName
Whoami SceneName
UseCache bool
Ui *ebitenui.UI
Lost bool
}
func NewNextlevelScene(game *Game, lost bool) Scene {
scene := &NextlevelScene{
Whoami: Nextlevel,
Game: game,
Next: Nextlevel,
Lost: lost,
}
scene.SetupUI()
return scene
}
func (scene *NextlevelScene) GetNext() SceneName {
return scene.Next
}
func (scene *NextlevelScene) ResetNext() {
scene.Next = scene.Whoami
}
func (scene *NextlevelScene) SetNext(next SceneName) {
slog.Debug("select setnext", "next", next)
scene.Next = next
}
func (scene *NextlevelScene) Clearscreen() bool {
// both level_scene AND the popup must not clear to get an actual popup
return false
}
func (scene *NextlevelScene) Update() error {
scene.Ui.Update()
return nil
}
func (scene *NextlevelScene) 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 *NextlevelScene) SetupUI() {
blue := color.RGBA{0, 255, 128, 255}
gameobserver := observers.GetGameObserver(scene.Game.World)
rowContainer := gameui.NewRowContainer(false)
labeltext := "Success"
switch scene.Lost {
case true:
labeltext = "Failure"
}
buttonRetry := gameui.NewMenuButton("Retry", *assets.FontRenderer.FontNormal,
func(args *widget.ButtonClickedEventArgs) {
scene.SetNext(Play)
gameobserver.Retry = true
})
buttonNext := gameui.NewMenuButton("Next Level", *assets.FontRenderer.FontNormal,
func(args *widget.ButtonClickedEventArgs) {
scene.SetNext(Play)
gameobserver.Retry = false
})
buttonAbort := gameui.NewMenuButton("Abort", *assets.FontRenderer.FontNormal,
func(args *widget.ButtonClickedEventArgs) {
scene.SetNext(Menu)
})
label := widget.NewText(
widget.TextOpts.Text(labeltext, *assets.FontRenderer.FontBig, blue),
widget.TextOpts.Position(widget.TextPositionCenter, widget.TextPositionCenter),
)
rowContainer.AddChild(label)
rowContainer.AddChild(buttonNext)
rowContainer.AddChild(buttonRetry)
rowContainer.AddChild(buttonAbort)
scene.Ui = &ebitenui.UI{
Container: rowContainer.Container(),
}
}

View File

@ -5,13 +5,14 @@ import (
)
const (
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
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
Nextlevel // displayed after loose or win
)
// Wrapper for different screens to be shown, as Welcome, Options,

View File

@ -48,6 +48,11 @@ func NewGrid(world *ecs.World,
components.Renderable,
components.Collectible](world)
obsmapper := generic.NewMap3[
components.Position,
components.Renderable,
components.Obstacle](world)
var pos *components.Position
var render *components.Renderable
var speed *components.Speed
@ -66,10 +71,14 @@ func NewGrid(world *ecs.World,
pos, _, render, speed, _ = playermapper.Get(entity)
playerobserver.AddEntity(entity)
speed.Value = config.PLAYERSPEED
slog.Debug("player start pos", "X", point.X*tilesize, "Y", point.Y*tilesize, "Z", 191)
slog.Debug("player start pos", "X", point.X*tilesize,
"Y", point.Y*tilesize, "Z", 191)
case tile.Collectible:
entity := colmapper.New()
pos, render, _ = colmapper.Get(entity)
case tile.Obstacle:
entity := obsmapper.New()
pos, render, _ = obsmapper.Get(entity)
default:
log.Fatalln("unsupported tile type encountered")
}

View File

@ -9,9 +9,12 @@ import (
// Used for global game state
type GameObserver struct {
CurrentLevel, Width, Height, Cellsize, Score int
StopTimer *components.Timer
Lost bool // set to true if player is struck or something, by default: win!
CurrentLevel, Width int
Height, Cellsize, Score int
StopTimer *components.Timer
Lost bool // set to true if player is struck or something, by default: win!
Retry bool
NextlevelText string
}
func NewGameObserver(world *ecs.World, startlevel, width, height, cellsize int) *GameObserver {
@ -34,3 +37,7 @@ func GetGameObserver(world *ecs.World) *GameObserver {
observer := world.Resources().Get(observerID).(*GameObserver)
return observer
}
func (observer *GameObserver) Gameover() {
observer.Lost = true
}

BIN
src/star.xcf Normal file

Binary file not shown.

View File

@ -30,6 +30,7 @@ func NewCollectibleSystem(world *ecs.World) *CollectibleSystem {
func (system *CollectibleSystem) Update() {
playerobserver := observers.GetPlayerObserver(system.World)
gameobserver := observers.GetGameObserver(system.World)
posID := ecs.ComponentID[components.Position](system.World)
veloID := ecs.ComponentID[components.Velocity](system.World)
@ -40,7 +41,7 @@ func (system *CollectibleSystem) Update() {
query := system.Selector.Query(system.World)
numcollectibles := query.Count()
if numcollectibles == 0 {
if numcollectibles == 0 || gameobserver.Lost {
query.Close()
return
}

117
systems/obstacle_system.go Normal file
View File

@ -0,0 +1,117 @@
package systems
import (
"log/slog"
"openquell/assets"
"openquell/components"
. "openquell/components"
"openquell/config"
. "openquell/config"
"openquell/observers"
"github.com/hajimehoshi/ebiten/v2"
"github.com/mlange-42/arche/ecs"
"github.com/mlange-42/arche/generic"
)
type ObstacleSystem struct {
World *ecs.World
Selector *generic.Filter3[Position, Obstacle, Renderable]
}
func NewObstacleSystem(world *ecs.World) *ObstacleSystem {
system := &ObstacleSystem{
Selector: generic.NewFilter3[Position, Obstacle, Renderable](),
World: world,
}
return system
}
func (system *ObstacleSystem) Update() {
playerobserver := observers.GetPlayerObserver(system.World)
gameobserver := observers.GetGameObserver(system.World)
if gameobserver.Lost {
return
}
posID := ecs.ComponentID[components.Position](system.World)
veloID := ecs.ComponentID[components.Velocity](system.World)
EntitiesToRemove := []ecs.Entity{}
query := system.Selector.Query(system.World)
gameover := false
for query.Next() {
obsposition, obstacle, _ := query.Get()
for player := range playerobserver.Entities {
playerposition := (*Position)(system.World.Get(player, posID))
playervelocity := (*Velocity)(system.World.Get(player, veloID))
ok, _ := playerposition.Intersects(obsposition, playervelocity)
if ok {
slog.Debug("bumped into obstacle", "obstacle", obstacle)
EntitiesToRemove = append(EntitiesToRemove, player)
gameover = true
}
}
}
for _, entity := range EntitiesToRemove {
system.World.RemoveEntity(entity)
}
if gameover {
// winner, winner, chicken dinner!
timer := gameobserver.StopTimer
if !timer.Running {
timer.Start(LEVEL_END_WAIT)
}
gameobserver.Gameover()
}
}
func (system *ObstacleSystem) Draw(screen *ebiten.Image) {
// write the movable tiles
op := &ebiten.DrawImageOptions{}
query := system.Selector.Query(system.World)
for query.Next() {
pos, _, sprite := query.Get()
op.GeoM.Reset()
op.GeoM.Translate(float64(pos.X), float64(pos.Y))
screen.DrawImage(sprite.Image, op)
}
}
func (system *ObstacleSystem) AddParticle(position *components.Position) {
particleobserver := observers.GetParticleObserver(system.World)
ptmapper := generic.NewMap3[
components.Position,
components.Particle,
components.Timer,
](system.World)
entity := ptmapper.New()
pos, particle, timer := ptmapper.Get(entity)
particleobserver.AddEntity(entity)
particle.Index = assets.Tiles['*'].Particle
particle.Particles = assets.Tiles['*'].Particles
pos.Update(
position.X-(16), // FIXME: use global tilesize!
position.Y-(16),
64,
)
timer.Start(config.PARTICLE_LOOPWAIT)
}

View File

@ -15,6 +15,7 @@ type PlayerSystem struct {
Selector *generic.Filter5[Position, Velocity, Player, Renderable, Speed]
Particle *ParticleSystem
Collectible *CollectibleSystem
Obstacle *ObstacleSystem
Grid *GridSystem
}
@ -23,6 +24,7 @@ func NewPlayerSystem(world *ecs.World, grid *GridSystem) *PlayerSystem {
Selector: generic.NewFilter5[Position, Velocity, Player, Renderable, Speed](),
Particle: NewParticleSystem(world, grid.Tilesize),
Collectible: NewCollectibleSystem(world),
Obstacle: NewObstacleSystem(world),
Grid: grid,
World: world,
}
@ -75,6 +77,7 @@ func (system PlayerSystem) Update() error {
system.Particle.Update()
system.Collectible.Update()
system.Obstacle.Update()
return nil
}
@ -93,6 +96,7 @@ func (system *PlayerSystem) Draw(screen *ebiten.Image) {
screen.DrawImage(sprite.Image, op)
}
system.Obstacle.Draw(screen)
system.Collectible.Draw(screen)
system.Particle.Draw(screen)
}