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

@ -33,3 +33,9 @@ if inpututil.IsKeyJustPressed(ebiten.KeyS) {
the edge of the obstacle and not inside as it is now
- 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 #
#> S <#
# #

View File

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

View File

@ -25,18 +25,42 @@ type Tile struct {
Id byte
Sprite *ebiten.Image
Class string
Solid bool
Player bool
IsPrimary bool
Renderable bool
Velocity bool
Collectible bool
Transient bool
Solid bool // wall brick
Player bool // player sphere
IsPrimary bool // primary player sphere
Renderable bool // visible, has sprite
Velocity bool // movable
Collectible bool // collectible, vanishes once collected
Transient bool // turns into brick wall when traversed
Destroyable bool // turns into empty floor when bumped into twice
Particle int // -1=unused, 0-3 = show image of slice
Tiles []*ebiten.Image
Tiles []*ebiten.Image // has N sprites
TileNames []string // same thing, only the names
Obstacle bool
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 (
@ -128,7 +152,7 @@ func NewTileTranswall(class []string) *Tile {
}
return &Tile{
Id: '*',
Id: 't',
Class: "transwall",
Solid: false,
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
type TileRegistry map[byte]*Tile
@ -164,7 +209,8 @@ type RawLevel struct {
func InitTiles() TileRegistry {
return TileRegistry{
' ': {Id: ' ', Class: "floor", Renderable: false},
'#': NewTileBlock("block-grey32"),
//'#': NewTileBlock("block-grey32"),
'#': NewTileBlock("block-greycolored"),
'B': NewTileBlock("block-orange-32"),
'S': NewTilePlayer(Primary),
's': NewTilePlayer(Secondary),
@ -183,6 +229,7 @@ func InitTiles() TileRegistry {
"particle-ring-6",
}),
'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 {
// 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)
}

View File

@ -3,6 +3,7 @@ package game
import (
"image"
"log"
"log/slog"
"openquell/assets"
"openquell/components"
"openquell/grid"
@ -15,12 +16,16 @@ import (
"github.com/mlange-42/arche/ecs"
)
type Map map[image.Point]*assets.Tile
type BackupMap map[image.Point]assets.Tile
type Level struct {
Cellsize, Width, Height int
World *ecs.World
Name string
Description string
Mapslice map[image.Point]*assets.Tile
Mapslice Map
BackupMapslice Map
GridContainer *grid.GridContainer
Systems []systems.System
Grid *grid.Grid
@ -45,8 +50,13 @@ func NewLevel(game *Game, cellsize int, plan *assets.RawLevel) *Level {
systemlist = append(systemlist, systems.NewTransientSystem(game.World, gridcontainer))
systemlist = append(systemlist, systems.NewDestroyableSystem(game.World, gridcontainer))
mapslice, backupmap := LevelToSlice(game, plan, cellsize)
return &Level{
Mapslice: LevelToSlice(game, plan, cellsize),
Mapslice: mapslice,
BackupMapslice: backupmap,
Cellsize: cellsize,
World: game.World,
Width: game.ScreenWidth,
@ -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) {
// generic variant does not work here:
// selector := generic.NewFilter1[components.Position]()
@ -92,15 +108,20 @@ func (level *Level) SetupGrid(game *Game) {
playerobserver := observers.GetPlayerObserver(level.World)
playerobserver.RemoveEntities()
// get rid of possibly manipulated map
level.RestoreMap()
// setup world
slog.Debug("new grid?")
level.GridContainer.SetGrid(
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
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
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") {
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)
}
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.Transient](world)
doormapper := generic.NewMap3[
components.Position,
components.Renderable,
components.Destroyable](world)
var pos *components.Position
var vel *components.Velocity
var render *components.Renderable
var speed *components.Speed
var transient *components.Transient
var player *components.Player
var destroyable *components.Destroyable
playerobserver := observers.GetPlayerObserver(world)
obstacleobserver := observers.GetObstacleObserver(world)
@ -99,6 +105,10 @@ func NewGrid(world *ecs.World,
entity := transmapper.New()
pos, render, transient = transmapper.Get(entity)
transient.Sprites = tile.TileNames
case tile.Destroyable:
entity := doormapper.New()
pos, render, destroyable = doormapper.Get(entity)
destroyable.Sprites = tile.Tiles
default:
log.Fatalln("unsupported tile type encountered")
}
@ -143,7 +153,15 @@ func (grid *Grid) GetTile(
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[
components.Position,
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 (
"image"
"image/draw"
"log/slog"
. "openquell/components"
"github.com/hajimehoshi/ebiten/v2"
@ -44,8 +45,8 @@ func (system *GridSystem) Update() error { return nil }
func (system *GridSystem) Draw(screen *ebiten.Image) {
op := &ebiten.DrawImageOptions{}
query := system.Selector.Query(system.World)
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
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() {
sprite, pos, _ := query.Get()
//slog.Debug("drawing sprite", "sprite", sprite, "point", pos.Point())
draw.Draw(
system.Cache,
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
// collision detection works
system.GridContainer.Grid.SetTile(
system.GridContainer.Grid.SetSolidTile(
assets.NewTileBlock(convertible.NewSprite),
convertible.Position.Point(),
)