diff --git a/assets/levels/00-deadly-obstacles.lvl b/assets/levels/00-deadly-obstacles.lvl index 07e6280..710e398 100644 --- a/assets/levels/00-deadly-obstacles.lvl +++ b/assets/levels/00-deadly-obstacles.lvl @@ -4,8 +4,8 @@ Background: background-lila ####### - # v # # # + # t # #> S <# # # # ^ # diff --git a/assets/levels/01-friendly-obstacles.lvl b/assets/levels/01-friendly-obstacles.lvl index 2db6d91..5470e2e 100644 --- a/assets/levels/01-friendly-obstacles.lvl +++ b/assets/levels/01-friendly-obstacles.lvl @@ -3,13 +3,13 @@ Background: background-lila - ####### - # ^ # - # # - #< S ># - # # - #^ v# - ####### + ############# + # # + #> S # # + # ># ># + #< # # + # v# <# + ############# diff --git a/assets/loader-levels.go b/assets/loader-levels.go index d02b5cc..c05a8d4 100644 --- a/assets/loader-levels.go +++ b/assets/loader-levels.go @@ -30,8 +30,10 @@ type Tile struct { Renderable bool Velocity bool Collectible bool + Transient bool Particle int // -1=unused, 0-3 = show image of slice - Particles []*ebiten.Image + Tiles []*ebiten.Image + TileNames []string // same thing, only the names Obstacle bool Direction int // obstacles } @@ -77,6 +79,7 @@ func NewTileObstacle(class string, direction int) *Tile { Renderable: true, Obstacle: true, Direction: direction, + Velocity: true, } } @@ -93,7 +96,28 @@ func NewTileParticle(class []string) *Tile { Solid: false, Renderable: false, Particle: 0, - Particles: sprites, + Tiles: sprites, + } +} + +func NewTileTranswall(class []string) *Tile { + sprites := []*ebiten.Image{} + names := []string{} + + for _, sprite := range class { + sprites = append(sprites, Assets[sprite]) + names = append(names, sprite) + } + + return &Tile{ + Id: '*', + Class: "transwall", + Solid: false, + Renderable: true, + Transient: true, + Tiles: sprites, + Sprite: sprites[0], // initially use the first + TileNames: names, } } @@ -123,6 +147,7 @@ func InitTiles() TileRegistry { return TileRegistry{ ' ': {Id: ' ', Class: "floor", Renderable: false}, '#': NewTileBlock("block-grey32"), + 'B': NewTileBlock("block-orange-32"), 'S': NewTilePlayer(), 'o': NewTileCollectible("collectible-orange"), '+': NewTileObstacle("obstacle-star", config.All), @@ -138,6 +163,7 @@ func InitTiles() TileRegistry { "particle-ring-5", "particle-ring-6", }), + 't': NewTileTranswall([]string{"transwall", "block-orange-32"}), } } diff --git a/assets/sprites/transwall.png b/assets/sprites/transwall.png new file mode 100644 index 0000000..b6a8338 Binary files /dev/null and b/assets/sprites/transwall.png differ diff --git a/components/components.go b/components/components.go index 882f5fa..7189a40 100644 --- a/components/components.go +++ b/components/components.go @@ -11,9 +11,9 @@ type Renderable struct { } type Particle struct { - Show bool - Index int - Particles []*ebiten.Image + Show bool + Index int + Tiles []*ebiten.Image } type Speed struct { @@ -26,4 +26,7 @@ type Solid struct{} type Floor struct{} type Player struct{} type Collectible struct{} -type Obstacle struct{} + +type Obstacle struct { + Direction int +} diff --git a/components/position.go b/components/position.go index 5766b26..b9036a0 100644 --- a/components/position.go +++ b/components/position.go @@ -3,7 +3,6 @@ package components import ( "fmt" "image" - "log/slog" . "openquell/config" ) @@ -90,13 +89,14 @@ func (tile *Position) Intersects(moving *Position, velocity *Velocity) (bool, *P is := tile.Rect.Bounds().Intersect(object.Rect.Bounds()) if is != image.ZR { - slog.Debug("Intersect", - "velocity", velocity.Data, - "player", moving.Rect, - "moved player", object.Rect, - "collision at", is, - ) - + /* + slog.Debug("Intersect", + "velocity", velocity.Data, + "player", moving.Rect, + "moved player", object.Rect, + "collision at", is, + ) + */ // collision, snap into neighbouring tile depending on the direction switch velocity.Direction { case West: diff --git a/components/transient.go b/components/transient.go new file mode 100644 index 0000000..dfb7e1e --- /dev/null +++ b/components/transient.go @@ -0,0 +1,24 @@ +package components + +import ( + "log" +) + +type Transient struct { + Activated bool + Sprites []string + Current int // sprite index +} + +func (trans *Transient) GetNext() string { + if len(trans.Sprites) > trans.Current { + trans.Current++ + return trans.Sprites[trans.Current] + } + + log.Fatalf("not enough sprites in transient tile, have %d sprites, index requested: %d", + len(trans.Sprites), trans.Current+1, + ) + + return "" +} diff --git a/components/velocity.go b/components/velocity.go index 3da8c58..f7d1cd0 100644 --- a/components/velocity.go +++ b/components/velocity.go @@ -6,8 +6,20 @@ import ( // movement in relation to position type Velocity struct { - Data Position - Direction int + Data Position + Direction int + PointingAt int +} + +func (velocity *Velocity) Set(new *Velocity) { + velocity.Direction = new.Direction + velocity.Data.Set(&new.Data) +} + +func (velocity *Velocity) ResetDirectionAndStop() { + velocity.Data.X = 0 + velocity.Data.Y = 0 + velocity.Direction = velocity.PointingAt } func (velocity *Velocity) Change(direction int) { diff --git a/game/game.go b/game/game.go index ffe2248..9188ece 100644 --- a/game/game.go +++ b/game/game.go @@ -33,6 +33,7 @@ func NewGame(width, height, cellsize, startlevel int, startscene SceneName) *Gam observers.NewPlayerObserver(&world) observers.NewParticleObserver(&world) + observers.NewObstacleObserver(&world) game.Observer = observers.NewGameObserver(&world, startlevel, width, height, cellsize) game.Scenes[Welcome] = NewWelcomeScene(game) diff --git a/game/level_scene.go b/game/level_scene.go index e550fdb..d181702 100644 --- a/game/level_scene.go +++ b/game/level_scene.go @@ -70,7 +70,8 @@ func (scene *LevelScene) Update() error { 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) + slog.Debug("level", "current", scene.CurrentLevel, + "next", scene.Game.Observer.CurrentLevel) scene.CurrentLevel = scene.Game.Observer.CurrentLevel scene.Levels[scene.CurrentLevel].SetupGrid(scene.Game) } diff --git a/game/levels.go b/game/levels.go index c38d356..fbe4a25 100644 --- a/game/levels.go +++ b/game/levels.go @@ -8,6 +8,7 @@ import ( "openquell/grid" "openquell/observers" "openquell/systems" + "openquell/util" "strings" "github.com/hajimehoshi/ebiten/v2" @@ -79,9 +80,11 @@ func (level *Level) SetupGrid(game *Game) { playerobserver.RemoveEntities() // setup world - level.GridSystem.SetGrid(grid.NewGrid(game.World, level.Cellsize, level.Width, level.Height, level.Mapslice)) + level.GridSystem.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 { size := game.ScreenWidth * game.ScreenHeight mapslice := make(map[image.Point]*assets.Tile, size) @@ -93,6 +96,10 @@ func LevelToSlice(game *Game, level *assets.RawLevel, tilesize int) map[image.Po } for x, char := range line { + if !util.Exists(assets.Tiles, byte(char)) { + log.Fatalf("unregistered tile type %c encountered", char) + } + mapslice[image.Point{x, y}] = assets.Tiles[byte(char)] } } diff --git a/grid/grid.go b/grid/grid.go index 869a2bb..9ad6ba0 100644 --- a/grid/grid.go +++ b/grid/grid.go @@ -3,7 +3,6 @@ package grid import ( "image" "log" - "log/slog" "openquell/assets" "openquell/components" "openquell/config" @@ -14,6 +13,7 @@ import ( ) type Grid struct { + World *ecs.World Width int Height int Size int @@ -48,18 +48,26 @@ func NewGrid(world *ecs.World, components.Renderable, components.Collectible](world) - obsmapper := generic.NewMap4[ + obsmapper := generic.NewMap5[ components.Position, components.Velocity, components.Renderable, + components.Speed, components.Obstacle](world) + transmapper := generic.NewMap3[ + components.Position, + components.Renderable, + components.Transient](world) + var pos *components.Position var vel *components.Velocity var render *components.Renderable var speed *components.Speed + var transient *components.Transient playerobserver := observers.GetPlayerObserver(world) + obstacleobserver := observers.GetObstacleObserver(world) for point, tile := range mapslice { switch tile.Renderable { @@ -73,15 +81,20 @@ 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) case tile.Collectible: entity := colmapper.New() pos, render, _ = colmapper.Get(entity) case tile.Obstacle: entity := obsmapper.New() - pos, vel, render, _ = obsmapper.Get(entity) + pos, vel, render, speed, _ = obsmapper.Get(entity) vel.Direction = tile.Direction + vel.PointingAt = tile.Direction + speed.Value = config.PLAYERSPEED + obstacleobserver.AddEntity(entity) + case tile.Transient: + entity := transmapper.New() + pos, render, transient = transmapper.Get(entity) + transient.Sprites = tile.TileNames default: log.Fatalln("unsupported tile type encountered") } @@ -106,6 +119,7 @@ func NewGrid(world *ecs.World, Width: width, Height: height, Map: mapslice, + World: world, } } @@ -122,3 +136,19 @@ func (grid *Grid) GetTile( tile := grid.Map[newpoint] return tile } + +func (grid *Grid) SetTile(tile *assets.Tile, point image.Point) { + solidmapper := generic.NewMap4[ + components.Position, + components.Renderable, + components.Tilish, + components.Solid](grid.World) + + grid.Map[point] = tile + + entity := solidmapper.New() + pos, render, _, _ := solidmapper.Get(entity) + + render.Image = tile.Sprite + pos.Update(point.X*grid.Tilesize, point.Y*grid.Tilesize, grid.Tilesize) +} diff --git a/observers/obstacle_observer.go b/observers/obstacle_observer.go new file mode 100644 index 0000000..52a71a5 --- /dev/null +++ b/observers/obstacle_observer.go @@ -0,0 +1,54 @@ +package observers + +import ( + "github.com/mlange-42/arche/ecs" + "github.com/mlange-42/arche/ecs/event" + "github.com/mlange-42/arche/generic" + "github.com/mlange-42/arche/listener" +) + +// will be added as an ecs.Resource to the world so we can use it in +// CollisionSystem to dynamically make it appear or disappear +type ObstacleObserver struct { + // we only have one obstacle so far, if we use multiple ones, turn + // this in to a map, see player_observer.go for an example + Entities map[ecs.Entity]int +} + +// Create a new resource of type PlayerObserver which will hold all +// obstacle entities. We also add a Listener which looks for +// EntityRemoved events and if a player gets removed, we also remove +// it from the Entity map in our observer. +func NewObstacleObserver(world *ecs.World) { + observer := &ObstacleObserver{} + observer.Entities = make(map[ecs.Entity]int) + + resmanger := generic.NewResource[ObstacleObserver](world) + resmanger.Add(observer) + + listen := listener.NewCallback( + func(world *ecs.World, event ecs.EntityEvent) { + observerID := ecs.ResourceID[ObstacleObserver](world) + observer := world.Resources().Get(observerID).(*ObstacleObserver) + observer.RemoveEntity(event.Entity) + }, + + event.EntityRemoved, + ) + + world.SetListener(&listen) +} + +func GetObstacleObserver(world *ecs.World) *ObstacleObserver { + observerID := ecs.ResourceID[ObstacleObserver](world) + observer := world.Resources().Get(observerID).(*ObstacleObserver) + return observer +} + +func (observer *ObstacleObserver) AddEntity(entity ecs.Entity) { + observer.Entities[entity] = 1 +} + +func (observer *ObstacleObserver) RemoveEntity(entity ecs.Entity) { + observer.Entities = make(map[ecs.Entity]int) +} diff --git a/src/transwall.xcf b/src/transwall.xcf new file mode 100644 index 0000000..1a6f907 Binary files /dev/null and b/src/transwall.xcf differ diff --git a/systems/collectible_system.go b/systems/collectible_system.go index 65a16f8..958b964 100644 --- a/systems/collectible_system.go +++ b/systems/collectible_system.go @@ -109,7 +109,7 @@ func (system *CollectibleSystem) AddParticle(position *components.Position) { particleobserver.AddEntity(entity) particle.Index = assets.Tiles['*'].Particle - particle.Particles = assets.Tiles['*'].Particles + particle.Tiles = assets.Tiles['*'].Tiles pos.Update( position.X-(16), // FIXME: use global tilesize! diff --git a/systems/grid_system.go b/systems/grid_system.go index 689d045..fefaa09 100644 --- a/systems/grid_system.go +++ b/systems/grid_system.go @@ -18,6 +18,7 @@ type GridSystem struct { Selector *generic.Filter3[Renderable, Position, Solid] UseCache bool Cache *ebiten.Image + Count int // register tile count, invalidates cache Background *ebiten.Image Width, Height, TilesX, TilesY, Tilesize int Grid *grid.Grid @@ -52,12 +53,13 @@ func (system *GridSystem) Update() {} func (system *GridSystem) Draw(screen *ebiten.Image) { op := &ebiten.DrawImageOptions{} + query := system.Selector.Query(system.World) - if !system.UseCache { + if !system.UseCache || query.Count() != system.Count { // map not cached or cacheable, write it to the cache draw.Draw(system.Cache, system.Background.Bounds(), system.Background, image.ZP, draw.Src) - query := system.Selector.Query(system.World) + system.Count = query.Count() for query.Next() { sprite, pos, _ := query.Get() @@ -76,6 +78,7 @@ func (system *GridSystem) Draw(screen *ebiten.Image) { // use the cached map op.GeoM.Reset() screen.DrawImage(system.Cache, op) + query.Close() } } @@ -88,7 +91,7 @@ func (system *GridSystem) GetSolidNeighborPosition( return false, nil } - // set to true, ifwe are already on the last tile in the current + // set to true, if we are already on the last tile in the current // direction, i.e. on the edge of the grid edge := true neighborpos := position.Point() diff --git a/systems/obstacle_system.go b/systems/obstacle_system.go index b045cc4..749abb6 100644 --- a/systems/obstacle_system.go +++ b/systems/obstacle_system.go @@ -15,15 +15,16 @@ import ( ) type ObstacleSystem struct { - World *ecs.World - Selector *generic.Filter4[Position, Velocity, Obstacle, Renderable] - PreviousFreePos *components.Position + World *ecs.World + Selector *generic.Filter5[Position, Velocity, Obstacle, Renderable, Speed] + Grid *GridSystem } -func NewObstacleSystem(world *ecs.World) *ObstacleSystem { +func NewObstacleSystem(world *ecs.World, grid *GridSystem) *ObstacleSystem { system := &ObstacleSystem{ - Selector: generic.NewFilter4[Position, Velocity, Obstacle, Renderable](), + Selector: generic.NewFilter5[Position, Velocity, Obstacle, Renderable, Speed](), World: world, + Grid: grid, } return system @@ -32,6 +33,7 @@ func NewObstacleSystem(world *ecs.World) *ObstacleSystem { func (system *ObstacleSystem) Update() { playerobserver := observers.GetPlayerObserver(system.World) gameobserver := observers.GetGameObserver(system.World) + obstacleobserver := observers.GetObstacleObserver(system.World) if gameobserver.Lost { return @@ -46,27 +48,67 @@ func (system *ObstacleSystem) Update() { gameover := false for query.Next() { - obsposition, obsvelocity, obstacle, _ := query.Get() + obsposition, obsvelocity, _, _, speed := query.Get() + // check if one player has bumped into current obstacle for player := range playerobserver.Entities { playerposition := (*Position)(system.World.Get(player, posID)) playervelocity := (*Velocity)(system.World.Get(player, veloID)) ok, newpos := obsposition.Intersects(playerposition, playervelocity) if ok { - slog.Debug("bumped into obstacle", "obstacle", obstacle) + // slog.Debug("bumped into obstacle", "obstacle", obstacle) if CheckObstacleSide(playervelocity, obsvelocity.Direction) { + // player died EntitiesToRemove = append(EntitiesToRemove, player) gameover = true } else { - playervelocity.Change(Stop) + // bumped into nonlethal obstacle side, stop the + // player, set the obstacle in motion, if possible slog.Debug("bump not die", "originalpos", playerposition) + obsvelocity.Set(playervelocity) + playervelocity.Change(Stop) playerposition.Set(newpos) - slog.Debug("bump not die", "newpos", newpos) } } } + + // check if current obstacle bumped into another obstacle + for foreign_obstacle := range obstacleobserver.Entities { + if foreign_obstacle == query.Entity() { + // don't check obstacle against itself + continue + } + + foreign_obstacle_position := (*Position)(system.World.Get(foreign_obstacle, posID)) + + ok, newpos := foreign_obstacle_position.Intersects(obsposition, obsvelocity) + if ok { + //slog.Debug("bumped into foreign obstacle", "obstacle", foreign_obstacle) + obsposition.Set(newpos) + obsvelocity.ResetDirectionAndStop() + } + } + + // FIXME: this is the same loop as in player_system, unite the + // two, just iterate over all entities with pos,vel,render, dammit + if obsvelocity.Moving() { + ok, tilepos := system.Grid.GetSolidNeighborPosition(obsposition, obsvelocity, true) + if ok { + intersects, newpos := tilepos.Intersects(obsposition, obsvelocity) + if intersects { + // slog.Debug("collision with foreign obstacle detected", "tile", + // tilepos, "obs", obsposition, "new", newpos) + + obsposition.Set(newpos) + obsvelocity.ResetDirectionAndStop() + } + } + + obsposition.Move(obsvelocity, speed) + } + } for _, entity := range EntitiesToRemove { @@ -91,7 +133,7 @@ func (system *ObstacleSystem) Draw(screen *ebiten.Image) { query := system.Selector.Query(system.World) for query.Next() { - pos, _, _, sprite := query.Get() + pos, _, _, sprite, _ := query.Get() op.GeoM.Reset() op.GeoM.Translate(float64(pos.X), float64(pos.Y)) @@ -114,7 +156,7 @@ func (system *ObstacleSystem) AddParticle(position *components.Position) { particleobserver.AddEntity(entity) particle.Index = assets.Tiles['*'].Particle - particle.Particles = assets.Tiles['*'].Particles + particle.Tiles = assets.Tiles['*'].Tiles pos.Update( position.X-(16), // FIXME: use global tilesize! diff --git a/systems/particle_system.go b/systems/particle_system.go index ef65b81..5eb4c2a 100644 --- a/systems/particle_system.go +++ b/systems/particle_system.go @@ -40,7 +40,7 @@ func (system *ParticleSystem) Update() { if timer.IsReady() { switch { // particle shows from earlier tick, animate - case particle.Index > -1 && particle.Index < len(particle.Particles)-1: + case particle.Index > -1 && particle.Index < len(particle.Tiles)-1: particle.Index++ timer.Start(config.PARTICLE_LOOPWAIT) default: @@ -69,7 +69,7 @@ func (system *ParticleSystem) Draw(screen *ebiten.Image) { if particle.Show { op.GeoM.Reset() op.GeoM.Translate(float64(pos.X), float64(pos.Y)) - screen.DrawImage(particle.Particles[particle.Index], op) + screen.DrawImage(particle.Tiles[particle.Index], op) } } } diff --git a/systems/player_system.go b/systems/player_system.go index f2970ea..d042766 100644 --- a/systems/player_system.go +++ b/systems/player_system.go @@ -1,7 +1,6 @@ package systems import ( - "log/slog" . "openquell/components" . "openquell/config" @@ -16,6 +15,7 @@ type PlayerSystem struct { Particle *ParticleSystem Collectible *CollectibleSystem Obstacle *ObstacleSystem + Transient *TransientSystem Grid *GridSystem } @@ -24,7 +24,8 @@ 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), + Obstacle: NewObstacleSystem(world, grid), + Transient: NewTransientSystem(world, grid), Grid: grid, World: world, } @@ -55,15 +56,15 @@ func (system PlayerSystem) Update() error { if velocity.Moving() { ok, newpos := system.Grid.BumpEdge(playerposition, velocity) if ok { - slog.Debug("falling off the edge", "newpos", newpos) + //slog.Debug("falling off the edge", "newpos", newpos) playerposition.Set(newpos) } else { ok, tilepos := system.Grid.GetSolidNeighborPosition(playerposition, velocity, true) if ok { intersects, newpos := tilepos.Intersects(playerposition, velocity) if intersects { - slog.Debug("collision detected", "tile", - tilepos, "player", playerposition, "new", newpos) + // slog.Debug("collision detected", "tile", + // tilepos, "player", playerposition, "new", newpos) playerposition.Set(newpos) velocity.Change(Stop) @@ -76,6 +77,7 @@ func (system PlayerSystem) Update() error { system.Particle.Update() // may set player position system.Obstacle.Update() system.Collectible.Update() + system.Transient.Update() query = system.Selector.Query(system.World) for query.Next() { @@ -104,4 +106,5 @@ func (system *PlayerSystem) Draw(screen *ebiten.Image) { system.Collectible.Draw(screen) system.Particle.Draw(screen) system.Obstacle.Draw(screen) + system.Transient.Draw(screen) } diff --git a/systems/transient_system.go b/systems/transient_system.go new file mode 100644 index 0000000..ea4e7a5 --- /dev/null +++ b/systems/transient_system.go @@ -0,0 +1,116 @@ +package systems + +import ( + "log/slog" + "openquell/assets" + "openquell/components" + . "openquell/components" + "openquell/observers" + + "github.com/hajimehoshi/ebiten/v2" + "github.com/mlange-42/arche/ecs" + "github.com/mlange-42/arche/generic" +) + +type TransientSystem struct { + World *ecs.World + Selector *generic.Filter3[Position, Renderable, Transient] + Grid *GridSystem + SolidMapper generic.Map4[ // needed for replacement + components.Position, + components.Renderable, + components.Tilish, + components.Solid] +} + +type TransientToWall struct { + Entity ecs.Entity + NewSprite string + Position components.Position +} + +func NewTransientSystem(world *ecs.World, grid *GridSystem) *TransientSystem { + solidmapper := generic.NewMap4[ + components.Position, + components.Renderable, + components.Tilish, + components.Solid](world) + + system := &TransientSystem{ + Selector: generic.NewFilter3[Position, Renderable, Transient](), + World: world, + Grid: grid, + SolidMapper: solidmapper, + } + + return system +} + +func (system *TransientSystem) Update() { + 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) + + EntitiestoMakeSolid := []TransientToWall{} + + for query.Next() { + transientposition, _, transient := query.Get() + + for player := range playerobserver.Entities { + playerposition := (*Position)(system.World.Get(player, posID)) + playervelocity := (*Velocity)(system.World.Get(player, veloID)) + + ok, _ := transientposition.Intersects(playerposition, playervelocity) + if ok { + // display the transient sprite as long as the player crosses it + transient.Activated = true + } else { + // the player crossed the transient wall completely + if transient.Activated { + EntitiestoMakeSolid = append(EntitiestoMakeSolid, TransientToWall{ + Entity: query.Entity(), + Position: *transientposition, + NewSprite: transient.GetNext(), + }) + slog.Debug("done transient", "transient", transientposition) + } + } + } + } + + for _, convertible := range EntitiestoMakeSolid { + // remove transient entity + system.World.RemoveEntity(convertible.Entity) + + // replace with solid entity + entity := system.SolidMapper.New() + pos, render, _, _ := system.SolidMapper.Get(entity) + + // set it up apropriately + pos.Set(&convertible.Position) + render.Image = assets.Assets[convertible.NewSprite] + + // also setup the grid tile with a new solid, so that + // collision detection works + system.Grid.Grid.SetTile( + assets.NewTileBlock(convertible.NewSprite), + convertible.Position.Point(), + ) + } +} + +func (system *TransientSystem) 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) + } +}