added hidden doors/destroyable walls, fixed game reloading (tile clone)

This commit is contained in:
Thomas von Dein 2024-02-25 14:05:44 +01:00
parent ab07bc23e3
commit b0a8060d5b
15 changed files with 271 additions and 39 deletions

View File

@ -32,4 +32,10 @@ if inpututil.IsKeyJustPressed(ebiten.KeyS) {
velocity and included snap in so that the player ends up right on velocity and included snap in so that the player ends up right on
the edge of the obstacle and not inside as it is now the edge of the obstacle and not inside as it is now
- Check player-player collisions! - Check player-player collisions!
- Do all collision detections in ONE system
- do not use the map anymore for collision detection
- check swept AABB instead of my collision detection, to allow for higher speeds

View File

@ -4,7 +4,7 @@ Background: background-lila
####### #######
# # #o #
# t # # t #
#> S <# #> S <#
# # # #

View File

@ -4,13 +4,13 @@ Background: background-lila
########## ########W#
# v# # v #
# # # #
#S s# #S s#
# # ############
#^ # # o #
########## ######## #

View File

@ -25,18 +25,42 @@ type Tile struct {
Id byte Id byte
Sprite *ebiten.Image Sprite *ebiten.Image
Class string Class string
Solid bool Solid bool // wall brick
Player bool Player bool // player sphere
IsPrimary bool IsPrimary bool // primary player sphere
Renderable bool Renderable bool // visible, has sprite
Velocity bool Velocity bool // movable
Collectible bool Collectible bool // collectible, vanishes once collected
Transient bool Transient bool // turns into brick wall when traversed
Particle int // -1=unused, 0-3 = show image of slice Destroyable bool // turns into empty floor when bumped into twice
Tiles []*ebiten.Image Particle int // -1=unused, 0-3 = show image of slice
TileNames []string // same thing, only the names Tiles []*ebiten.Image // has N sprites
Obstacle bool TileNames []string // same thing, only the names
Direction int // obstacles Obstacle bool // is an obstacle/enemy
Direction int // obstacle business end shows into this direction
}
func (tile *Tile) Clone() *Tile {
newtile := &Tile{
Id: tile.Id,
Sprite: tile.Sprite,
Class: tile.Class,
Solid: tile.Solid,
Player: tile.Player,
IsPrimary: tile.IsPrimary,
Renderable: tile.Renderable,
Velocity: tile.Velocity,
Collectible: tile.Collectible,
Transient: tile.Transient,
Destroyable: tile.Destroyable,
Particle: tile.Particle,
Tiles: tile.Tiles,
TileNames: tile.TileNames,
Obstacle: tile.Obstacle,
Direction: tile.Direction,
}
return newtile
} }
const ( const (
@ -128,7 +152,7 @@ func NewTileTranswall(class []string) *Tile {
} }
return &Tile{ return &Tile{
Id: '*', Id: 't',
Class: "transwall", Class: "transwall",
Solid: false, Solid: false,
Renderable: true, Renderable: true,
@ -139,6 +163,27 @@ func NewTileTranswall(class []string) *Tile {
} }
} }
func NewTileHiddenDoor(class []string) *Tile {
sprites := []*ebiten.Image{}
names := []string{}
for _, sprite := range class {
sprites = append(sprites, Assets[sprite])
names = append(names, sprite)
}
return &Tile{
Id: 'W',
Class: "hiddendoor",
Solid: false,
Renderable: true,
Destroyable: true,
Tiles: sprites,
Sprite: sprites[0], // initially use the first
TileNames: names,
}
}
// used to map level data bytes to actual tiles // used to map level data bytes to actual tiles
type TileRegistry map[byte]*Tile type TileRegistry map[byte]*Tile
@ -164,7 +209,8 @@ type RawLevel struct {
func InitTiles() TileRegistry { func InitTiles() TileRegistry {
return TileRegistry{ return TileRegistry{
' ': {Id: ' ', Class: "floor", Renderable: false}, ' ': {Id: ' ', Class: "floor", Renderable: false},
'#': NewTileBlock("block-grey32"), //'#': NewTileBlock("block-grey32"),
'#': NewTileBlock("block-greycolored"),
'B': NewTileBlock("block-orange-32"), 'B': NewTileBlock("block-orange-32"),
'S': NewTilePlayer(Primary), 'S': NewTilePlayer(Primary),
's': NewTilePlayer(Secondary), 's': NewTilePlayer(Secondary),
@ -183,6 +229,7 @@ func InitTiles() TileRegistry {
"particle-ring-6", "particle-ring-6",
}), }),
't': NewTileTranswall([]string{"transwall", "block-orange-32"}), 't': NewTileTranswall([]string{"transwall", "block-orange-32"}),
'W': NewTileHiddenDoor([]string{"block-greycolored", "block-greycolored-damaged"}),
} }
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

29
components/destroyable.go Normal file
View File

@ -0,0 +1,29 @@
package components
import (
"log"
"github.com/hajimehoshi/ebiten/v2"
)
// A hidden door in a wall. If the player bumps into it once, it shows
// damage and it vanishes the next time.
type Destroyable struct {
Activated bool
Sprites []*ebiten.Image
Current int // sprite index
}
func (door *Destroyable) GetNext() *ebiten.Image {
if len(door.Sprites) > door.Current {
door.Current++
return door.Sprites[door.Current]
}
log.Fatalf("not enough sprites in transient tile, have %d sprites, index requested: %d",
len(door.Sprites), door.Current+1,
)
return nil
}

View File

@ -96,7 +96,6 @@ func (game *Game) Update() error {
if next == Play { if next == Play {
// fresh setup of actual level every time we enter the play scene // fresh setup of actual level every time we enter the play scene
//game.Scenes[Play] = NewLevelScene(game, gameobserver.CurrentLevel)
game.Scenes[Play].SetLevel(gameobserver.CurrentLevel) game.Scenes[Play].SetLevel(gameobserver.CurrentLevel)
} }

View File

@ -3,6 +3,7 @@ package game
import ( import (
"image" "image"
"log" "log"
"log/slog"
"openquell/assets" "openquell/assets"
"openquell/components" "openquell/components"
"openquell/grid" "openquell/grid"
@ -15,12 +16,16 @@ import (
"github.com/mlange-42/arche/ecs" "github.com/mlange-42/arche/ecs"
) )
type Map map[image.Point]*assets.Tile
type BackupMap map[image.Point]assets.Tile
type Level struct { type Level struct {
Cellsize, Width, Height int Cellsize, Width, Height int
World *ecs.World World *ecs.World
Name string Name string
Description string Description string
Mapslice map[image.Point]*assets.Tile Mapslice Map
BackupMapslice Map
GridContainer *grid.GridContainer GridContainer *grid.GridContainer
Systems []systems.System Systems []systems.System
Grid *grid.Grid Grid *grid.Grid
@ -45,16 +50,21 @@ func NewLevel(game *Game, cellsize int, plan *assets.RawLevel) *Level {
systemlist = append(systemlist, systems.NewTransientSystem(game.World, gridcontainer)) systemlist = append(systemlist, systems.NewTransientSystem(game.World, gridcontainer))
systemlist = append(systemlist, systems.NewDestroyableSystem(game.World, gridcontainer))
mapslice, backupmap := LevelToSlice(game, plan, cellsize)
return &Level{ return &Level{
Mapslice: LevelToSlice(game, plan, cellsize), Mapslice: mapslice,
Cellsize: cellsize, BackupMapslice: backupmap,
World: game.World, Cellsize: cellsize,
Width: game.ScreenWidth, World: game.World,
Height: game.ScreenHeight, Width: game.ScreenWidth,
Description: plan.Description, Height: game.ScreenHeight,
Name: plan.Name, Description: plan.Description,
GridContainer: gridcontainer, Name: plan.Name,
Systems: systemlist, GridContainer: gridcontainer,
Systems: systemlist,
} }
} }
@ -77,6 +87,12 @@ func (level *Level) Position2Point(position *components.Position) image.Point {
} }
} }
func (level *Level) RestoreMap() {
for point, tile := range level.BackupMapslice {
level.Mapslice[point] = tile.Clone()
}
}
func (level *Level) SetupGrid(game *Game) { func (level *Level) SetupGrid(game *Game) {
// generic variant does not work here: // generic variant does not work here:
// selector := generic.NewFilter1[components.Position]() // selector := generic.NewFilter1[components.Position]()
@ -92,15 +108,20 @@ func (level *Level) SetupGrid(game *Game) {
playerobserver := observers.GetPlayerObserver(level.World) playerobserver := observers.GetPlayerObserver(level.World)
playerobserver.RemoveEntities() playerobserver.RemoveEntities()
// get rid of possibly manipulated map
level.RestoreMap()
// setup world // setup world
slog.Debug("new grid?")
level.GridContainer.SetGrid( level.GridContainer.SetGrid(
grid.NewGrid(game.World, level.Cellsize, level.Width, level.Height, level.Mapslice)) grid.NewGrid(game.World, level.Cellsize, level.Width, level.Height, level.Mapslice))
} }
// parses a RawLevel and generates a mapslice from it, which is being used as grid // parses a RawLevel and generates a mapslice from it, which is being used as grid
func LevelToSlice(game *Game, level *assets.RawLevel, tilesize int) map[image.Point]*assets.Tile { func LevelToSlice(game *Game, level *assets.RawLevel, tilesize int) (Map, Map) {
size := game.ScreenWidth * game.ScreenHeight size := game.ScreenWidth * game.ScreenHeight
mapslice := make(map[image.Point]*assets.Tile, size) mapslice := make(Map, size)
backupmap := make(Map, size)
for y, line := range strings.Split(string(level.Data), "\n") { for y, line := range strings.Split(string(level.Data), "\n") {
if len(line) != game.ScreenWidth/tilesize && y < game.ScreenHeight/tilesize { if len(line) != game.ScreenWidth/tilesize && y < game.ScreenHeight/tilesize {
@ -113,9 +134,11 @@ func LevelToSlice(game *Game, level *assets.RawLevel, tilesize int) map[image.Po
log.Fatalf("unregistered tile type %c encountered", char) log.Fatalf("unregistered tile type %c encountered", char)
} }
mapslice[image.Point{x, y}] = assets.Tiles[byte(char)] tile := assets.Tiles[byte(char)]
mapslice[image.Point{x, y}] = tile
backupmap[image.Point{x, y}] = tile.Clone()
} }
} }
return mapslice return mapslice, backupmap
} }

View File

@ -61,12 +61,18 @@ func NewGrid(world *ecs.World,
components.Renderable, components.Renderable,
components.Transient](world) components.Transient](world)
doormapper := generic.NewMap3[
components.Position,
components.Renderable,
components.Destroyable](world)
var pos *components.Position var pos *components.Position
var vel *components.Velocity var vel *components.Velocity
var render *components.Renderable var render *components.Renderable
var speed *components.Speed var speed *components.Speed
var transient *components.Transient var transient *components.Transient
var player *components.Player var player *components.Player
var destroyable *components.Destroyable
playerobserver := observers.GetPlayerObserver(world) playerobserver := observers.GetPlayerObserver(world)
obstacleobserver := observers.GetObstacleObserver(world) obstacleobserver := observers.GetObstacleObserver(world)
@ -99,6 +105,10 @@ func NewGrid(world *ecs.World,
entity := transmapper.New() entity := transmapper.New()
pos, render, transient = transmapper.Get(entity) pos, render, transient = transmapper.Get(entity)
transient.Sprites = tile.TileNames transient.Sprites = tile.TileNames
case tile.Destroyable:
entity := doormapper.New()
pos, render, destroyable = doormapper.Get(entity)
destroyable.Sprites = tile.Tiles
default: default:
log.Fatalln("unsupported tile type encountered") log.Fatalln("unsupported tile type encountered")
} }
@ -143,7 +153,15 @@ func (grid *Grid) GetTile(
return tile return tile
} }
func (grid *Grid) SetTile(tile *assets.Tile, point image.Point) { func (grid *Grid) RemoveTile(point image.Point) {
delete(grid.Map, point)
}
func (grid *Grid) SetFloorTile(point image.Point) {
grid.Map[point] = assets.Tiles[' ']
}
func (grid *Grid) SetSolidTile(tile *assets.Tile, point image.Point) {
solidmapper := generic.NewMap4[ solidmapper := generic.NewMap4[
components.Position, components.Position,
components.Renderable, components.Renderable,

BIN
src/block-grey-damage.xcf Normal file

Binary file not shown.

Binary file not shown.

View File

@ -0,0 +1,108 @@
package systems
import (
"log/slog"
"openquell/components"
. "openquell/components"
. "openquell/config"
"openquell/grid"
"openquell/observers"
"github.com/hajimehoshi/ebiten/v2"
"github.com/mlange-42/arche/ecs"
"github.com/mlange-42/arche/generic"
)
type DestroyableSystem struct {
World *ecs.World
Selector *generic.Filter3[Position, Renderable, Destroyable]
GridContainer *grid.GridContainer
SolidMapper generic.Map4[ // needed for replacement
components.Position,
components.Renderable,
components.Tilish,
components.Solid]
}
type DoorToRemove struct {
Entity ecs.Entity
Position *components.Position
}
func NewDestroyableSystem(world *ecs.World, gridcontainer *grid.GridContainer) System {
solidmapper := generic.NewMap4[
components.Position,
components.Renderable,
components.Tilish,
components.Solid](world)
system := &DestroyableSystem{
Selector: generic.NewFilter3[Position, Renderable, Destroyable](),
World: world,
GridContainer: gridcontainer,
SolidMapper: solidmapper,
}
return system
}
func (system *DestroyableSystem) Update() error {
playerobserver := observers.GetPlayerObserver(system.World)
posID := ecs.ComponentID[components.Position](system.World)
veloID := ecs.ComponentID[components.Velocity](system.World)
query := system.Selector.Query(system.World)
EntitiestoRemove := []*DoorToRemove{}
for query.Next() {
doorposition, renderable, door := query.Get()
for player := range playerobserver.Entities {
playerposition := (*Position)(system.World.Get(player, posID))
playervelocity := (*Velocity)(system.World.Get(player, veloID))
ok, newpos := doorposition.Intersects(playerposition, playervelocity)
if ok {
// player bumped into hidden wall, activate it, snap in player
slog.Debug("bump not die", "originalpos", playerposition)
playervelocity.Change(Stop)
playerposition.Set(newpos)
if door.Activated {
// player bumps into the door a second time, now hide it
EntitiestoRemove = append(EntitiestoRemove,
&DoorToRemove{Entity: query.Entity(), Position: doorposition})
} else {
slog.Debug("activating destroyable", "doorpos", doorposition)
door.Activated = true
renderable.Image = door.GetNext()
}
}
}
}
for _, door := range EntitiestoRemove {
slog.Debug("hiding destroyable", "doorpos", door.Position.Point())
// remove door entity
system.World.RemoveEntity(door.Entity)
}
return nil
}
func (system *DestroyableSystem) Draw(screen *ebiten.Image) {
// write transients (these are no tiles!)
op := &ebiten.DrawImageOptions{}
query := system.Selector.Query(system.World)
for query.Next() {
pos, render, _ := query.Get()
op.GeoM.Reset()
op.GeoM.Translate(float64(pos.X), float64(pos.Y))
screen.DrawImage(render.Image, op)
}
}

View File

@ -3,6 +3,7 @@ package systems
import ( import (
"image" "image"
"image/draw" "image/draw"
"log/slog"
. "openquell/components" . "openquell/components"
"github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2"
@ -44,8 +45,8 @@ func (system *GridSystem) Update() error { return nil }
func (system *GridSystem) Draw(screen *ebiten.Image) { func (system *GridSystem) Draw(screen *ebiten.Image) {
op := &ebiten.DrawImageOptions{} op := &ebiten.DrawImageOptions{}
query := system.Selector.Query(system.World) query := system.Selector.Query(system.World)
if !system.UseCache || query.Count() != system.Count { if !system.UseCache || query.Count() != system.Count {
slog.Debug("entity number changes", "old", system.Count, "current", query.Count())
// map not cached or cacheable, write it to the cache // map not cached or cacheable, write it to the cache
draw.Draw(system.Cache, system.Background.Bounds(), system.Background, image.ZP, draw.Src) draw.Draw(system.Cache, system.Background.Bounds(), system.Background, image.ZP, draw.Src)
@ -54,6 +55,7 @@ func (system *GridSystem) Draw(screen *ebiten.Image) {
for query.Next() { for query.Next() {
sprite, pos, _ := query.Get() sprite, pos, _ := query.Get()
//slog.Debug("drawing sprite", "sprite", sprite, "point", pos.Point())
draw.Draw( draw.Draw(
system.Cache, system.Cache,
image.Rect(pos.X, pos.Y, pos.X+pos.Cellsize, pos.Y+pos.Cellsize), image.Rect(pos.X, pos.Y, pos.X+pos.Cellsize, pos.Y+pos.Cellsize),

View File

@ -95,7 +95,7 @@ func (system *TransientSystem) Update() error {
// also setup the grid tile with a new solid, so that // also setup the grid tile with a new solid, so that
// collision detection works // collision detection works
system.GridContainer.Grid.SetTile( system.GridContainer.Grid.SetSolidTile(
assets.NewTileBlock(convertible.NewSprite), assets.NewTileBlock(convertible.NewSprite),
convertible.Position.Point(), convertible.Position.Point(),
) )