You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
366 lines
8.9 KiB
366 lines
8.9 KiB
package system |
|
|
|
import ( |
|
"image" |
|
"image/color" |
|
"time" |
|
|
|
"code.rocketnine.space/tslocum/gohan" |
|
"code.rocketnine.space/tslocum/monovania/component" |
|
"code.rocketnine.space/tslocum/monovania/engine" |
|
"code.rocketnine.space/tslocum/monovania/world" |
|
"github.com/hajimehoshi/ebiten/v2" |
|
) |
|
|
|
type MovementSystem struct { |
|
ScreenW, ScreenH float64 |
|
|
|
OnGround int |
|
OnLadder int |
|
|
|
Jumping bool |
|
LastJump time.Time |
|
|
|
collisionRects []image.Rectangle |
|
|
|
ladderRects []image.Rectangle |
|
|
|
fireRects []image.Rectangle |
|
|
|
debugCollisionRects []gohan.Entity |
|
debugLadderRects []gohan.Entity |
|
debugFireRects []gohan.Entity |
|
} |
|
|
|
func NewMovementSystem() *MovementSystem { |
|
s := &MovementSystem{ |
|
OnGround: -1, |
|
OnLadder: -1, |
|
} |
|
|
|
w := world.World |
|
|
|
// Cache collision rects. |
|
|
|
m := w.Map |
|
for _, layer := range m.Layers { |
|
collision := layer.Properties.GetBool("collision") |
|
if !collision { |
|
continue |
|
} |
|
|
|
for y := 0; y < m.Height; y++ { |
|
for x := 0; x < m.Width; x++ { |
|
t := layer.Tiles[y*m.Width+x] |
|
if t == nil || t.Nil { |
|
continue // No tile at this position. |
|
} |
|
|
|
if t.ID == world.FireTileA || t.ID == world.FireTileB || t.ID == world.FireTileC { |
|
continue // Fire collision rects are handled separately. |
|
} |
|
|
|
gx, gy := world.TileToGameCoords(x, y) |
|
s.collisionRects = append(s.collisionRects, image.Rect(int(gx), int(gy), int(gx)+16, int(gy)+16)) |
|
} |
|
} |
|
} |
|
|
|
// Cache ladder rects. |
|
|
|
for _, layer := range m.Layers { |
|
ladder := layer.Properties.GetBool("ladder") |
|
if !ladder { |
|
continue |
|
} |
|
|
|
for y := 0; y < m.Height; y++ { |
|
for x := 0; x < m.Width; x++ { |
|
t := layer.Tiles[y*m.Width+x] |
|
if t == nil || t.Nil { |
|
continue // No tile at this position. |
|
} |
|
gx, gy := world.TileToGameCoords(x, y) |
|
s.ladderRects = append(s.ladderRects, image.Rect(int(gx), int(gy), int(gx)+16, int(gy)+16)) |
|
} |
|
} |
|
} |
|
|
|
// Cache fire rects. |
|
|
|
for _, layer := range world.World.ObjectGroups { |
|
if layer.Name != "FIRES" { |
|
continue |
|
} |
|
|
|
for _, obj := range layer.Objects { |
|
rect := world.ObjectToRect(obj) |
|
|
|
// Adjust dimensions. |
|
rect.Min.X += 2 |
|
rect.Min.Y += 4 |
|
rect.Max.X -= 2 |
|
|
|
s.fireRects = append(s.fireRects, rect) |
|
} |
|
} |
|
|
|
s.UpdateDebugCollisionRects() |
|
|
|
return s |
|
} |
|
|
|
func drawDebugRect(r image.Rectangle, c color.Color, overrideColorScale bool) gohan.Entity { |
|
rectEntity := engine.Engine.NewEntity() |
|
|
|
rectImg := ebiten.NewImage(r.Dx(), r.Dy()) |
|
rectImg.Fill(c) |
|
|
|
engine.Engine.AddComponent(rectEntity, &component.PositionComponent{ |
|
X: float64(r.Min.X), |
|
Y: float64(r.Min.Y), |
|
}) |
|
|
|
engine.Engine.AddComponent(rectEntity, &component.SpriteComponent{ |
|
Image: rectImg, |
|
OverrideColorScale: overrideColorScale, |
|
}) |
|
|
|
return rectEntity |
|
} |
|
|
|
func (s *MovementSystem) removeDebugRects() { |
|
for _, e := range s.debugCollisionRects { |
|
engine.Engine.RemoveEntity(e) |
|
} |
|
s.debugCollisionRects = nil |
|
|
|
for _, e := range s.debugLadderRects { |
|
engine.Engine.RemoveEntity(e) |
|
} |
|
s.debugLadderRects = nil |
|
|
|
for _, e := range s.debugFireRects { |
|
engine.Engine.RemoveEntity(e) |
|
} |
|
s.debugFireRects = nil |
|
} |
|
|
|
func (s *MovementSystem) addDebugCollisionRects() { |
|
s.removeDebugRects() |
|
|
|
for _, rect := range s.collisionRects { |
|
c := color.RGBA{200, 200, 200, 150} |
|
debugRect := drawDebugRect(rect, c, true) |
|
s.debugCollisionRects = append(s.debugCollisionRects, debugRect) |
|
} |
|
|
|
for _, rect := range s.ladderRects { |
|
c := color.RGBA{0, 0, 200, 150} |
|
debugRect := drawDebugRect(rect, c, true) |
|
s.debugLadderRects = append(s.debugLadderRects, debugRect) |
|
} |
|
|
|
for _, rect := range s.fireRects { |
|
c := color.RGBA{200, 0, 0, 150} |
|
debugRect := drawDebugRect(rect, c, false) |
|
s.debugFireRects = append(s.debugFireRects, debugRect) |
|
} |
|
} |
|
|
|
func (s *MovementSystem) UpdateDebugCollisionRects() { |
|
if world.World.Debug < 2 { |
|
s.removeDebugRects() |
|
return |
|
} else if len(s.debugCollisionRects) == 0 { |
|
s.addDebugCollisionRects() |
|
} |
|
|
|
for i, debugRect := range s.debugCollisionRects { |
|
sprite := engine.Engine.Component(debugRect, component.SpriteComponentID).(*component.SpriteComponent) |
|
if s.OnGround == i { |
|
sprite.ColorScale = 1 |
|
} else { |
|
sprite.ColorScale = 0.4 |
|
} |
|
} |
|
|
|
for i, debugRect := range s.debugLadderRects { |
|
sprite := engine.Engine.Component(debugRect, component.SpriteComponentID).(*component.SpriteComponent) |
|
if s.OnLadder == i { |
|
sprite.ColorScale = 1 |
|
} else { |
|
sprite.ColorScale = 0.4 |
|
} |
|
} |
|
} |
|
|
|
func (s *MovementSystem) objectToGameCoords(x, y, height float64) (float64, float64) { |
|
return x, float64(world.World.Map.Height*16) - y - height |
|
} |
|
|
|
func (_ *MovementSystem) Needs() []gohan.ComponentID { |
|
return []gohan.ComponentID{ |
|
component.PositionComponentID, |
|
component.VelocityComponentID, |
|
} |
|
} |
|
|
|
func (_ *MovementSystem) Uses() []gohan.ComponentID { |
|
return nil |
|
} |
|
|
|
func (s *MovementSystem) checkFire(r image.Rectangle) { |
|
for _, fireRect := range s.fireRects { |
|
if r.Overlaps(fireRect) { |
|
//world.World.GameOver = true |
|
// TODO |
|
position := engine.Engine.Component(world.World.Player, component.PositionComponentID).(*component.PositionComponent) |
|
velocity := engine.Engine.Component(world.World.Player, component.VelocityComponentID).(*component.VelocityComponent) |
|
position.X, position.Y = world.World.SpawnX, world.World.SpawnY |
|
velocity.X, velocity.Y = 0, 0 |
|
return |
|
} |
|
} |
|
} |
|
|
|
func (s *MovementSystem) checkTriggers(r image.Rectangle) { |
|
for i, triggerRect := range world.World.TriggerRects { |
|
if r.Overlaps(triggerRect) { |
|
if world.World.TriggerNames[i] == "DOUBLEJUMP" { |
|
world.World.CanDoubleJump = true |
|
} else { |
|
panic("unknown trigger " + world.World.TriggerNames[i]) |
|
} |
|
|
|
// Remove trigger. |
|
engine.Engine.RemoveEntity(world.World.TriggerEntities[i]) |
|
|
|
world.World.TriggerRects = append(world.World.TriggerRects[:i], world.World.TriggerRects[i+1:]...) |
|
world.World.TriggerEntities = append(world.World.TriggerEntities[:i], world.World.TriggerEntities[i+1:]...) |
|
world.World.TriggerNames = append(world.World.TriggerNames[:i], world.World.TriggerNames[i+1:]...) |
|
return |
|
} |
|
} |
|
} |
|
|
|
func (s *MovementSystem) checkCollisions(r image.Rectangle) { |
|
s.checkFire(r) |
|
s.checkTriggers(r) |
|
} |
|
|
|
func (s *MovementSystem) Update(ctx *gohan.Context) error { |
|
lastOnGround := s.OnGround |
|
lastOnLadder := s.OnLadder |
|
|
|
position := component.Position(ctx) |
|
velocity := component.Velocity(ctx) |
|
|
|
bullet := ctx.Entity != world.World.Player |
|
|
|
onLadder := -1 |
|
playerRect := image.Rect(int(position.X), int(position.Y), int(position.X)+16, int(position.Y)+16) |
|
for i, rect := range s.ladderRects { |
|
if playerRect.Overlaps(rect) { |
|
onLadder = i |
|
|
|
// Grab the ladder when jumping on to it. |
|
if onLadder != lastOnLadder && !world.World.NoClip { |
|
velocity.Y = 0 |
|
//velocity.X /= 2 |
|
} |
|
break |
|
} |
|
} |
|
s.OnLadder = onLadder |
|
|
|
// Apply weight and gravity. |
|
|
|
const decel = 0.95 |
|
const ladderDecel = 0.9 |
|
const maxGravity = 9 |
|
const gravityAccel = 0.04 |
|
if !bullet { |
|
if s.OnLadder != -1 || world.World.Levitating || world.World.NoClip { |
|
velocity.X *= decel |
|
velocity.Y *= decel |
|
} else if s.OnLadder != -1 { // TODO incorrect |
|
velocity.X *= decel |
|
velocity.Y *= ladderDecel |
|
} else if velocity.Y < maxGravity { |
|
velocity.X *= decel |
|
|
|
if !s.Jumping { |
|
velocity.Y += gravityAccel |
|
} |
|
} |
|
} |
|
|
|
vx, vy := velocity.X, velocity.Y |
|
if world.World.Debug > 0 && ebiten.IsKeyPressed(ebiten.KeyShift) { |
|
vx, vy = vx*2, vy*2 |
|
} |
|
|
|
// Check collisions. |
|
|
|
var ( |
|
collideX = -1 |
|
collideY = -1 |
|
collideXY = -1 |
|
collideG = -1 |
|
) |
|
const gravityThreshold = 4 |
|
playerRectX := image.Rect(int(position.X+vx), int(position.Y), int(position.X+vx)+16, int(position.Y)+17) |
|
playerRectY := image.Rect(int(position.X), int(position.Y+vy), int(position.X)+16, int(position.Y+vy)+17) |
|
playerRectXY := image.Rect(int(position.X+vx), int(position.Y+vy), int(position.X+vx)+16, int(position.Y+vy)+17) |
|
playerRectG := image.Rect(int(position.X), int(position.Y+gravityThreshold), int(position.X)+16, int(position.Y+gravityThreshold)+17) |
|
for i, rect := range s.collisionRects { |
|
if world.World.NoClip { |
|
continue |
|
} |
|
if playerRectX.Overlaps(rect) { |
|
collideX = i |
|
s.checkCollisions(playerRectX) |
|
} |
|
if playerRectY.Overlaps(rect) { |
|
collideY = i |
|
s.checkCollisions(playerRectY) |
|
} |
|
if playerRectXY.Overlaps(rect) { |
|
collideXY = i |
|
s.checkCollisions(playerRectXY) |
|
} |
|
if playerRectG.Overlaps(rect) { |
|
collideG = i |
|
s.checkCollisions(playerRectG) |
|
} |
|
} |
|
if collideXY == -1 { |
|
position.X, position.Y = position.X+vx, position.Y+vy |
|
} else if collideX == -1 { |
|
position.X = position.X + vx |
|
velocity.Y = 0 |
|
} else if collideY == -1 { |
|
position.Y = position.Y + vy |
|
velocity.X = 0 |
|
} else { |
|
velocity.X, velocity.Y = 0, 0 |
|
} |
|
s.OnGround = collideG |
|
// Reset jump counter. |
|
if s.OnGround != -1 && world.World.Jumps != 0 && time.Since(s.LastJump) >= 50*time.Millisecond { |
|
world.World.Jumps = 0 |
|
} |
|
|
|
// Update debug rects. |
|
|
|
if s.OnGround != lastOnGround || s.OnLadder != lastOnLadder { |
|
s.UpdateDebugCollisionRects() |
|
} |
|
|
|
return nil |
|
} |
|
|
|
func (_ *MovementSystem) Draw(_ *gohan.Context, screen *ebiten.Image) error { |
|
return gohan.ErrSystemWithoutDraw |
|
}
|
|
|