package systems import ( "fmt" "log/slog" "openquell/components" . "openquell/components" . "openquell/config" "openquell/grid" "openquell/observers" "openquell/util" "github.com/hajimehoshi/ebiten/v2" "github.com/hajimehoshi/ebiten/v2/ebitenutil" "github.com/mlange-42/arche/ecs" "github.com/mlange-42/arche/generic" input "github.com/quasilyte/ebitengine-input" ) type PlayerSystem struct { World *ecs.World Selector *generic.Filter4[Position, Velocity, Player, Renderable] GridContainer *grid.GridContainer Width, Height int Input *input.Handler } func NewPlayerSystem(world *ecs.World, gridcontainer *grid.GridContainer, width, height int, handler *input.Handler) System { system := &PlayerSystem{ Selector: generic.NewFilter4[Position, Velocity, Player, Renderable](), GridContainer: gridcontainer, World: world, Width: width, Height: height, Input: handler, } return system } func PlayerBumpEdgeResponder( pos *components.Position, vel *components.Velocity, newpos *components.Position) { pos.Set(newpos) } func PlayerBumpWallResponder( pos *components.Position, vel *components.Velocity, newpos *components.Position) { slog.Debug("(2) PlayerBumpWallResponder", "old", pos.String(), "new", newpos.String()) pos.Set(newpos) vel.Change(Stop) } func (system PlayerSystem) Update() error { var EntitiesToRemove []ecs.Entity // check if we need to switch player[s] system.SwitchPlayers() // check player movements etc query := system.Selector.Query(system.World) count := query.Count() for query.Next() { playerposition, velocity, player, _ := query.Get() if !player.IsPrimary { continue } // check if the user alters or initiates movement, only // changes player direction system.CheckMovement(playerposition, velocity, player) // check if player collides with walls or edges if velocity.Moving() { slog.Debug("(2) checking grid collision") system.GridContainer.Grid.CheckGridCollision( playerposition, velocity, PlayerBumpEdgeResponder, PlayerBumpWallResponder) } if count > 1 { // check if player collides with another player, fuse them if any EntitiesToRemove = system.CheckPlayerCollision(playerposition, velocity, query.Entity()) if len(EntitiesToRemove) > 0 { slog.Debug("other player collision") } } else { // only 1 player left or one is max EntitiesToRemove = system.CheckPlayerLooping( playerposition, velocity, player, query.Entity()) if len(EntitiesToRemove) > 0 { slog.Debug("player loops", "entities", EntitiesToRemove) } } if !velocity.Moving() { // disable loop detection player.LoopCount = 0 } } // 2nd pass: move player after obstacle or collectible updates query = system.Selector.Query(system.World) for query.Next() { playerposition, velocity, _, _ := query.Get() oldpos := playerposition.String() if velocity.Moving() { playerposition.Move(velocity) slog.Debug("(4) moving player", "old", oldpos, "new", playerposition.String()) } } // we may have lost players, remove them here for _, entity := range EntitiesToRemove { slog.Debug("remove player", "ent", entity.IsZero()) system.World.RemoveEntity(entity) } return nil } func (system *PlayerSystem) 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) if util.DebugEnabled() { ebitenutil.DebugPrintAt(screen, pos.SmallString(), pos.X, pos.Y+16) // print player pos } } } func (system *PlayerSystem) SwitchPlayers() { // first check if we need to switch player switchable := false query := system.Selector.Query(system.World) count := query.Count() for query.Next() { _, _, player, _ := query.Get() if !player.IsPrimary { switchable = true } } if switchable { query := system.Selector.Query(system.World) for query.Next() { _, _, player, render := query.Get() if count == 1 && !player.IsPrimary { // there's only one player left, make it the primary one player.IsPrimary = true render.Image = player.SwitchSprite() } else { // many players, switch when requested if system.Input.ActionIsJustPressed(SwitchPlayer) { slog.Debug("switch players") if player.IsPrimary { player.IsPrimary = false render.Image = player.SwitchSprite() } else { player.IsPrimary = true render.Image = player.SwitchSprite() } } } } } } func (system *PlayerSystem) CheckMovement( position *components.Position, velocity *components.Velocity, player *components.Player) { moved := false observer := observers.GetGameObserver(system.World) if !velocity.Moving() { switch { case system.Input.ActionIsJustPressed(MoveRight): velocity.Change(East) moved = true case system.Input.ActionIsJustPressed(MoveLeft): velocity.Change(West) moved = true case system.Input.ActionIsPressed(MoveDown): velocity.Change(South) moved = true case system.Input.ActionIsJustPressed(MoveUp): velocity.Change(North) moved = true } if moved { // will be reset every time the user initiates player movement player.LoopPos.Set(position) player.LoopCount = 0 observer.AddMove() } } else { if util.DebugEnabled() { fmt.Println("------------------------") } slog.Debug("(1) player is at", "current", position.String()) } } func (system *PlayerSystem) CheckPlayerCollision( position *components.Position, velocity *components.Velocity, player ecs.Entity) []ecs.Entity { observer := observers.GetGameObserver(system.World) posID := ecs.ComponentID[components.Position](system.World) EntitiesToRemove := []ecs.Entity{} for _, otherplayer := range observer.GetPlayers() { if !system.World.Alive(otherplayer) { continue } if otherplayer == player { continue } otherposition := (*Position)(system.World.Get(otherplayer, posID)) ok, _ := otherposition.Intersects(position, velocity) if ok { // keep displaying it until the two fully intersect EntitiesToRemove = append(EntitiesToRemove, otherplayer) } } // FIXME: add an animation highlighting the fusion return EntitiesToRemove } func (system *PlayerSystem) CheckPlayerLooping( position *components.Position, velocity *components.Velocity, player *components.Player, entity ecs.Entity) []ecs.Entity { if player.LoopPos.Rect == nil { // hasn't moved return nil } EntitiesToRemove := []ecs.Entity{} ok, _ := player.LoopPos.Intersects(position, velocity) if ok { // Fatal: loop detected with last player player.LoopStart = true } else { // no intersection with old pos anymore if player.LoopStart { // loop detection active player.LoopCount++ max := system.Width if velocity.Direction == North || velocity.Direction == South { max = system.Height } max += system.GridContainer.Grid.Tilesize * 5 // we haved moved one time around the whole screen, loose if (player.LoopCount * velocity.Speed) > max { slog.Debug("loop?", "loopcount", player.LoopCount, "speed", velocity.Speed, "max", max) EntitiesToRemove = append(EntitiesToRemove, entity) } } } return EntitiesToRemove }