287 lines
		
	
	
		
			7.1 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			287 lines
		
	
	
		
			7.1 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package systems
 | |
| 
 | |
| import (
 | |
| 	"fmt"
 | |
| 	"log/slog"
 | |
| 	"openquell/components"
 | |
| 	. "openquell/components"
 | |
| 	. "openquell/config"
 | |
| 	"openquell/grid"
 | |
| 	"openquell/observers"
 | |
| 
 | |
| 	"github.com/hajimehoshi/ebiten/v2"
 | |
| 	"github.com/hajimehoshi/ebiten/v2/ebitenutil"
 | |
| 	"github.com/hajimehoshi/ebiten/v2/inpututil"
 | |
| 	"github.com/mlange-42/arche/ecs"
 | |
| 	"github.com/mlange-42/arche/generic"
 | |
| )
 | |
| 
 | |
| type PlayerSystem struct {
 | |
| 	World         *ecs.World
 | |
| 	Selector      *generic.Filter4[Position, Velocity, Player, Renderable]
 | |
| 	GridContainer *grid.GridContainer
 | |
| 	Width, Height int
 | |
| }
 | |
| 
 | |
| func NewPlayerSystem(world *ecs.World, gridcontainer *grid.GridContainer, width, height int) System {
 | |
| 	system := &PlayerSystem{
 | |
| 		Selector:      generic.NewFilter4[Position, Velocity, Player, Renderable](),
 | |
| 		GridContainer: gridcontainer,
 | |
| 		World:         world,
 | |
| 		Width:         width,
 | |
| 		Height:        height,
 | |
| 	}
 | |
| 
 | |
| 	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)
 | |
| 
 | |
| 		ebitenutil.DebugPrintAt(screen, pos.String(), pos.X, pos.Y) // 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 inpututil.IsKeyJustPressed(ebiten.KeyTab) {
 | |
| 					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 inpututil.IsKeyJustPressed(ebiten.KeyRight):
 | |
| 			velocity.Change(East)
 | |
| 			moved = true
 | |
| 		case inpututil.IsKeyJustPressed(ebiten.KeyLeft):
 | |
| 			velocity.Change(West)
 | |
| 			moved = true
 | |
| 		case inpututil.IsKeyJustPressed(ebiten.KeyDown):
 | |
| 			velocity.Change(South)
 | |
| 			moved = true
 | |
| 		case inpututil.IsKeyJustPressed(ebiten.KeyUp):
 | |
| 			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 {
 | |
| 		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
 | |
| }
 |