Browse Source

Add levitate ability

main
Trevor Slocum 1 month ago
parent
commit
2485cf9a20
  1. 4
      asset/asset.go
  2. BIN
      asset/image/weapons/bullet.png
  3. BIN
      asset/image/weapons/muzzle-flash.png
  4. BIN
      asset/image/weapons/uzi.png
  5. 2559
      asset/map/m1.tmx
  6. 8
      asset/ss_player.go
  7. 23
      component/destructible.go
  8. 3
      component/sprite.go
  9. 2
      component/weapon.go
  10. 2
      entity/bullet.go
  11. 1
      entity/player.go
  12. 1
      flags.go
  13. 15
      game/game.go
  14. 4
      go.mod
  15. 8
      go.sum
  16. 49
      system/input_fire.go
  17. 69
      system/input_move.go
  18. 257
      system/movement.go
  19. 18
      system/render.go
  20. 97
      system/rendermessage.go
  21. 39
      world/world.go

4
asset/asset.go

@ -17,6 +17,10 @@ var ImgBackground2 = LoadImage("image/szadiart-caves/background2.png")
var ImgBackground3 = LoadImage("image/szadiart-caves/background3.png")
var ImgBackground4 = LoadImage("image/szadiart-caves/background4a.png")
var ImgUzi = LoadImage("image/weapons/uzi.png")
var ImgBullet = LoadImage("image/weapons/bullet.png")
func LoadImage(p string) *ebiten.Image {
f, err := FS.Open(p)
if err != nil {

BIN
asset/image/weapons/bullet.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 KiB

BIN
asset/image/weapons/muzzle-flash.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

BIN
asset/image/weapons/uzi.png

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.7 KiB

2559
asset/map/m1.tmx

File diff suppressed because it is too large

8
asset/ss_player.go

@ -15,10 +15,6 @@ type PlayerSpriteSheet struct {
DuckR *ebiten.Image
WalkR1 *ebiten.Image
WalkR2 *ebiten.Image
IdleL *ebiten.Image
DuckL *ebiten.Image
WalkL1 *ebiten.Image
WalkL2 *ebiten.Image
}
// LoadPlayerSpriteSheet loads the embedded PlayerSpriteSheet.
@ -47,10 +43,6 @@ func LoadPlayerSpriteSheet() *PlayerSpriteSheet {
s.WalkR1 = spriteAt(1, 0)
s.WalkR2 = spriteAt(2, 0)
s.DuckR = spriteAt(3, 0)
s.IdleL = spriteAt(0, 1)
s.WalkL1 = spriteAt(1, 1)
s.WalkL2 = spriteAt(2, 1)
s.DuckL = spriteAt(3, 1)
return s
}

23
component/destructible.go

@ -0,0 +1,23 @@
package component
import (
"code.rocketnine.space/tslocum/gohan"
"code.rocketnine.space/tslocum/monovania/engine"
)
type DestructibleComponent struct {
}
var DestructibleComponentID = engine.Engine.NewComponentID()
func (p *DestructibleComponent) ComponentID() gohan.ComponentID {
return DestructibleComponentID
}
func Destructible(ctx *gohan.Context) *DestructibleComponent {
c, ok := ctx.Component(DestructibleComponentID).(*DestructibleComponent)
if !ok {
return nil
}
return c
}

3
component/sprite.go

@ -15,6 +15,9 @@ type SpriteComponent struct {
VerticalFlip bool
DiagonalFlip bool // TODO unimplemented
Overlay *ebiten.Image
OverlayX, OverlayY float64 // Overlay offset
Frame int
Frames []*ebiten.Image
FrameTime time.Duration

2
component/weapon.go

@ -8,7 +8,7 @@ import (
)
type WeaponComponent struct {
Ammo int
Equipped bool
Damage int

2
entity/bullet.go

@ -21,7 +21,7 @@ func NewBullet(x, y, xSpeed, ySpeed float64) gohan.Entity {
})
engine.Engine.AddComponent(bullet, &component.SpriteComponent{
Image: asset.ImgWhiteSquare,
Image: asset.ImgBullet,
})
engine.Engine.AddComponent(bullet, &component.BulletComponent{})

1
entity/player.go

@ -20,7 +20,6 @@ func NewPlayer(x, y float64) gohan.Entity {
engine.Engine.AddComponent(player, &component.VelocityComponent{})
weapon := &component.WeaponComponent{
Ammo: 1000,
Damage: 1,
FireRate: 100 * time.Millisecond,
BulletSpeed: 15,

1
flags.go

@ -52,5 +52,6 @@ func parseFlags() {
if noSplash {
world.World.GameStarted = true
world.World.MessageVisible = false
}
}

15
game/game.go

@ -7,18 +7,13 @@ import (
"os"
"sync"
"code.rocketnine.space/tslocum/monovania/engine"
"code.rocketnine.space/tslocum/monovania/world"
"code.rocketnine.space/tslocum/gohan"
"code.rocketnine.space/tslocum/monovania/asset"
"code.rocketnine.space/tslocum/monovania/component"
"code.rocketnine.space/tslocum/monovania/engine"
"code.rocketnine.space/tslocum/monovania/entity"
"code.rocketnine.space/tslocum/monovania/asset"
"code.rocketnine.space/tslocum/gohan"
"code.rocketnine.space/tslocum/monovania/system"
"code.rocketnine.space/tslocum/monovania/world"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/audio"
"golang.org/x/text/language"
@ -101,6 +96,8 @@ func NewGame() (*game, error) {
asset.ImgWhiteSquare.Fill(color.White)
world.World.SetMessage("<J> TO JUMP.\n<R> TO REWIND.\n<WASD> TO MOVE.")
return g, nil
}

4
go.mod

@ -15,8 +15,8 @@ require (
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20211213063430-748e38ca8aec // indirect
github.com/hajimehoshi/oto/v2 v2.1.0-alpha.4 // indirect
github.com/jezek/xgb v0.0.0-20210312150743-0e0f116e1240 // indirect
golang.org/x/exp v0.0.0-20211213173848-79cd87713b62 // indirect
golang.org/x/exp v0.0.0-20211214223157-bafe2e20209a // indirect
golang.org/x/mobile v0.0.0-20211207041440-4e6c2922fdee // indirect
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c // indirect
golang.org/x/sys v0.0.0-20211214150614-024a26f5d6e2 // indirect
golang.org/x/sys v0.0.0-20211214234402-4825e8c3871d // indirect
)

8
go.sum

@ -38,8 +38,8 @@ golang.org/x/crypto v0.0.0-20190510104115-cbcb75029529/go.mod h1:yigFU9vqHzYiE8U
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/exp v0.0.0-20190306152737-a1d7652674e8/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20190731235908-ec7cb31e5a56/go.mod h1:JhuoJpWY28nO4Vef9tZUw9qufEGTyX1+7lmHxV5q5G4=
golang.org/x/exp v0.0.0-20211213173848-79cd87713b62 h1:NLQyV2V75F1Y0l2EyOgXGh0Z44+AlPxVIN6BHOniazA=
golang.org/x/exp v0.0.0-20211213173848-79cd87713b62/go.mod h1:b9TAUYHmRtqA6klRHApnXMnj+OyLce4yF5cZCUbk2ps=
golang.org/x/exp v0.0.0-20211214223157-bafe2e20209a h1:AAl9OocndjmqsjP37NRIXRsHZooES2MX3Imla+GKe5o=
golang.org/x/exp v0.0.0-20211214223157-bafe2e20209a/go.mod h1:b9TAUYHmRtqA6klRHApnXMnj+OyLce4yF5cZCUbk2ps=
golang.org/x/image v0.0.0-20190227222117-0694c2d4d067/go.mod h1:kZ7UVZpmo3dzQBMxlp+ypCbDeSB+sBbTgSJuh5dn5js=
golang.org/x/image v0.0.0-20190703141733-d6a02ce849c9/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/image v0.0.0-20190802002840-cff245a6509b/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
@ -72,8 +72,8 @@ golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210809222454-d867a43fc93e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210917161153-d61c044b1678/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211214150614-024a26f5d6e2 h1:oJg+vmWs1UY4oSg6n1drFSkU2Nc48mxtz5qhA0HaG0I=
golang.org/x/sys v0.0.0-20211214150614-024a26f5d6e2/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211214234402-4825e8c3871d h1:1oIt9o40TWWI9FUaveVpUvBe13FNqBNVXy3ue2fcfkw=
golang.org/x/sys v0.0.0-20211214234402-4825e8c3871d/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=

49
system/input_fire.go

@ -27,6 +27,7 @@ func NewFireWeaponSystem(player gohan.Entity) *fireWeaponSystem {
func (_ *fireWeaponSystem) Needs() []gohan.ComponentID {
return []gohan.ComponentID{
component.PositionComponentID,
component.SpriteComponentID,
component.WeaponComponentID,
}
}
@ -35,55 +36,39 @@ func (_ *fireWeaponSystem) Uses() []gohan.ComponentID {
return nil
}
func (s *fireWeaponSystem) fire(weapon *component.WeaponComponent, position *component.PositionComponent, fireAngle float64) {
func (s *fireWeaponSystem) fire(weapon *component.WeaponComponent, position *component.PositionComponent, sprite *component.SpriteComponent, fireAngle float64) {
if time.Since(weapon.LastFire) < weapon.FireRate {
return
}
weapon.Ammo--
weapon.LastFire = time.Now()
speedX := math.Cos(fireAngle) * -weapon.BulletSpeed
speedY := math.Sin(fireAngle) * -weapon.BulletSpeed
bullet := entity.NewBullet(position.X, position.Y, speedX, speedY)
offsetX := 8.0
if sprite.HorizontalFlip {
offsetX = -24
}
const bulletOffsetY = -5
bullet := entity.NewBullet(position.X+offsetX, position.Y+bulletOffsetY, speedX, speedY)
_ = bullet
}
func (s *fireWeaponSystem) Update(ctx *gohan.Context) error {
return nil // TODO
weapon := component.Weapon(ctx)
if weapon.Ammo <= 0 {
if !weapon.Equipped {
return nil
}
position := component.Position(ctx)
if ebiten.IsMouseButtonPressed(ebiten.MouseButtonLeft) {
cursorX, cursorY := ebiten.CursorPosition()
fireAngle := angle(position.X, position.Y, float64(cursorX), float64(cursorY))
s.fire(weapon, position, fireAngle)
}
switch {
case ebiten.IsKeyPressed(ebiten.KeyLeft) && ebiten.IsKeyPressed(ebiten.KeyUp):
s.fire(weapon, position, math.Pi/4)
case ebiten.IsKeyPressed(ebiten.KeyLeft) && ebiten.IsKeyPressed(ebiten.KeyDown):
s.fire(weapon, position, -math.Pi/4)
case ebiten.IsKeyPressed(ebiten.KeyRight) && ebiten.IsKeyPressed(ebiten.KeyUp):
s.fire(weapon, position, math.Pi*.75)
case ebiten.IsKeyPressed(ebiten.KeyRight) && ebiten.IsKeyPressed(ebiten.KeyDown):
s.fire(weapon, position, -math.Pi*.75)
case ebiten.IsKeyPressed(ebiten.KeyLeft):
s.fire(weapon, position, 0)
case ebiten.IsKeyPressed(ebiten.KeyRight):
s.fire(weapon, position, math.Pi)
case ebiten.IsKeyPressed(ebiten.KeyUp):
s.fire(weapon, position, math.Pi/2)
case ebiten.IsKeyPressed(ebiten.KeyDown):
s.fire(weapon, position, -math.Pi/2)
if ebiten.IsKeyPressed(ebiten.KeyL) {
position := component.Position(ctx)
sprite := component.Sprite(ctx)
fireAngle := math.Pi
if sprite.HorizontalFlip {
fireAngle = 0
}
s.fire(weapon, position, sprite, fireAngle)
}
return nil
}

69
system/input_move.go

@ -1,6 +1,7 @@
package system
import (
"fmt"
"log"
"os"
"time"
@ -89,27 +90,33 @@ func (s *playerMoveSystem) Update(ctx *gohan.Context) error {
log.Printf("Spawn point set to %.0f,%.0f", world.World.SpawnX, world.World.SpawnY)
return nil
}
if ebiten.IsKeyPressed(ebiten.KeyControl) && inpututil.IsKeyJustPressed(ebiten.KeyK) {
if world.World.Keys < 3 {
world.World.Keys++
}
world.World.SetMessage(fmt.Sprintf("YOU NOW HAVE %d KEYS.", world.World.Keys))
return nil
}
if world.World.MessageVisible {
if inpututil.IsKeyJustPressed(ebiten.KeyEnter) {
world.World.MessageVisible = false
}
return nil
}
setWalkFrames := func() {
if world.World.NoClip {
return
}
sprite := component.Sprite(ctx)
if s.lastWalkDirL {
sprite.Frames = []*ebiten.Image{
asset.PlayerSS.WalkL1,
asset.PlayerSS.IdleL,
asset.PlayerSS.WalkL2,
asset.PlayerSS.IdleL,
}
} else {
sprite.Frames = []*ebiten.Image{
asset.PlayerSS.WalkR1,
asset.PlayerSS.IdleR,
asset.PlayerSS.WalkR2,
asset.PlayerSS.IdleR,
}
sprite.Frames = []*ebiten.Image{
asset.PlayerSS.WalkR1,
asset.PlayerSS.IdleR,
asset.PlayerSS.WalkR2,
asset.PlayerSS.IdleR,
}
sprite.HorizontalFlip = s.lastWalkDirL
sprite.NumFrames = 4
sprite.FrameTime = 150 * time.Millisecond
}
@ -117,19 +124,12 @@ func (s *playerMoveSystem) Update(ctx *gohan.Context) error {
setJumpAndIdleFrames := func() {
sprite := component.Sprite(ctx)
sprite.NumFrames = 0
if s.lastWalkDirL {
if (s.movement.OnGround == -1 && s.movement.OnLadder == -1) || s.movement.Jumping || world.World.NoClip {
sprite.Image = asset.PlayerSS.WalkL2
} else {
sprite.Image = asset.PlayerSS.IdleL
}
if (s.movement.OnGround == -1 && s.movement.OnLadder == -1) || s.movement.Jumping || world.World.NoClip {
sprite.Image = asset.PlayerSS.WalkR2
} else {
if (s.movement.OnGround == -1 && s.movement.OnLadder == -1) || s.movement.Jumping || world.World.NoClip {
sprite.Image = asset.PlayerSS.WalkR2
} else {
sprite.Image = asset.PlayerSS.IdleR
}
sprite.Image = asset.PlayerSS.IdleR
}
sprite.HorizontalFlip = s.lastWalkDirL
}
// Rewind time.
@ -237,11 +237,8 @@ func (s *playerMoveSystem) Update(ctx *gohan.Context) error {
// Duck and look down.
sprite := component.Sprite(ctx)
sprite.NumFrames = 0
if s.lastWalkDirL {
sprite.Image = asset.PlayerSS.DuckL
} else {
sprite.Image = asset.PlayerSS.DuckR
}
sprite.Image = asset.PlayerSS.DuckR
sprite.HorizontalFlip = s.lastWalkDirL
walkKeyPressed = true
if world.World.DuckStart == -1 {
@ -298,11 +295,12 @@ func (s *playerMoveSystem) Update(ctx *gohan.Context) error {
if !world.World.NoClip {
sprite := component.Sprite(ctx)
sprite.Frames = []*ebiten.Image{
asset.PlayerSS.WalkL1,
asset.PlayerSS.IdleL,
asset.PlayerSS.WalkL2,
asset.PlayerSS.IdleL,
asset.PlayerSS.WalkR1,
asset.PlayerSS.IdleR,
asset.PlayerSS.WalkR2,
asset.PlayerSS.IdleR,
}
sprite.HorizontalFlip = true
sprite.NumFrames = 4
sprite.FrameTime = 150 * time.Millisecond
}
@ -327,6 +325,7 @@ func (s *playerMoveSystem) Update(ctx *gohan.Context) error {
asset.PlayerSS.WalkR2,
asset.PlayerSS.IdleR,
}
sprite.HorizontalFlip = false
sprite.NumFrames = 4
sprite.FrameTime = 150 * time.Millisecond
}
@ -373,7 +372,7 @@ func (s *playerMoveSystem) Update(ctx *gohan.Context) error {
}
if world.World.Levitating {
if ebiten.IsKeyPressed(ebiten.KeyW) {
if ebiten.IsKeyPressed(ebiten.KeyJ) {
if velocity.Y > -maxLevitateSpeed {
velocity.Y -= moveSpeed
}

257
system/movement.go

@ -1,12 +1,14 @@
package system
import (
"fmt"
"image"
"image/color"
"math"
"time"
"code.rocketnine.space/tslocum/gohan"
"code.rocketnine.space/tslocum/monovania/asset"
"code.rocketnine.space/tslocum/monovania/component"
"code.rocketnine.space/tslocum/monovania/engine"
"code.rocketnine.space/tslocum/monovania/world"
@ -218,7 +220,9 @@ func (_ *MovementSystem) Needs() []gohan.ComponentID {
}
func (_ *MovementSystem) Uses() []gohan.ComponentID {
return nil
return []gohan.ComponentID{
component.WeaponComponentID,
}
}
func (s *MovementSystem) checkFire(ctx *gohan.Context, r image.Rectangle) {
@ -234,18 +238,56 @@ func (s *MovementSystem) checkFire(ctx *gohan.Context, r image.Rectangle) {
}
}
func (s *MovementSystem) checkTriggers(r image.Rectangle) {
func (s *MovementSystem) checkTriggers(ctx *gohan.Context, r image.Rectangle) {
for i, triggerRect := range world.World.TriggerRects {
if r.Overlaps(triggerRect) {
switch world.World.TriggerNames[i] {
case "DOUBLEJUMP":
world.World.CanDoubleJump = true
world.World.SetMessage("<J> TO DOUBLE JUMP.")
case "DASH":
world.World.CanDash = true
world.World.SetMessage("<K> TO DASH.")
case "LEVITATE":
world.World.CanLevitate = true
world.World.SetMessage("<J> TO LEVITATE AFTER DOUBLE JUMPING.")
case "KEY":
world.World.Keys++
if world.World.Keys == 1 {
world.World.SetMessage("FIRST EXIT KEY FOUND.")
} else if world.World.Keys == 1 {
world.World.SetMessage("SECOND EXIT KEY FOUND.")
} else {
world.World.SetMessage("FINAL EXIT KEY FOUND.")
}
case "UZI":
weapon := component.Weapon(ctx)
weapon.Equipped = true
sprite := engine.Engine.Component(world.World.Player, component.SpriteComponentID).(*component.SpriteComponent)
sprite.Overlay = asset.ImgUzi
sprite.OverlayX = 6
sprite.OverlayY = 7
world.World.SetMessage("<L> TO FIRE.")
case "EXIT":
if world.World.Keys < 3 {
position := component.Position(ctx)
velocity := component.Velocity(ctx)
position.X = position.X + 0.25
velocity.X = 0
world.World.SetMessage("THIS DOOR REQUIRES THREE KEYS.")
return
}
world.World.SetMessage("GAME OVER. YOU WIN!")
default:
panic("unknown trigger " + world.World.TriggerNames[i])
world.World.SetMessage(fmt.Sprintf("UNKNOWN TRIGGER '%s'.", world.World.TriggerNames[i]))
}
// Remove trigger.
@ -254,8 +296,6 @@ func (s *MovementSystem) checkTriggers(r image.Rectangle) {
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:]...)
// TODO Show ability info and usage popup.
return
}
}
@ -263,11 +303,11 @@ func (s *MovementSystem) checkTriggers(r image.Rectangle) {
func (s *MovementSystem) checkCollisions(ctx *gohan.Context, r image.Rectangle) {
s.checkFire(ctx, r)
s.checkTriggers(r)
s.checkTriggers(ctx, r)
}
func (s *MovementSystem) Update(ctx *gohan.Context) error {
if world.World.GameOver {
if world.World.MessageVisible || world.World.GameOver {
return nil
}
@ -283,17 +323,19 @@ func (s *MovementSystem) Update(ctx *gohan.Context) error {
return nil
}
// TODO apply left and right X collision adjustments (too large, can hang entirely off edge of cliff)
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
break
if ctx.Entity == world.World.Player {
// TODO apply left and right X collision adjustments (too large, can hang entirely off edge of cliff)
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
break
}
}
s.OnLadder = onLadder
}
s.OnLadder = onLadder
// Apply weight and gravity.
@ -320,89 +362,134 @@ func (s *MovementSystem) Update(ctx *gohan.Context) error {
}
vx, vy := velocity.X, velocity.Y
if world.World.Debug > 0 && ebiten.IsKeyPressed(ebiten.KeyShift) {
if (world.World.NoClip || 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(ctx, playerRectX)
}
if playerRectY.Overlaps(rect) {
collideY = i
s.checkCollisions(ctx, playerRectY)
}
if playerRectXY.Overlaps(rect) {
collideXY = i
s.checkCollisions(ctx, playerRectXY)
}
if playerRectG.Overlaps(rect) {
collideG = i
s.checkCollisions(ctx, playerRectG)
if ctx.Entity == world.World.Player {
var (
collideX = -1
collideY = -1
collideXY = -1
collideG = -1
)
const gravityThreshold = 4
playerRectX := image.Rect(int(position.X+vx), int(position.Y+1), int(position.X+vx)+16, int(position.Y)+17)
playerRectY := image.Rect(int(position.X), int(position.Y+1+vy), int(position.X)+16, int(position.Y+vy)+17)
playerRectXY := image.Rect(int(position.X+vx), int(position.Y+1+vy), int(position.X+vx)+16, int(position.Y+vy)+17)
playerRectG := image.Rect(int(position.X), int(position.Y+1+gravityThreshold), int(position.X)+16, int(position.Y+gravityThreshold)+17)
if !world.World.NoClip {
for i, rect := range s.collisionRects {
if playerRectX.Overlaps(rect) {
collideX = i
s.checkCollisions(ctx, playerRectX)
}
if playerRectY.Overlaps(rect) {
collideY = i
s.checkCollisions(ctx, playerRectY)
}
if playerRectXY.Overlaps(rect) {
collideXY = i
s.checkCollisions(ctx, playerRectXY)
}
if playerRectG.Overlaps(rect) {
collideG = i
s.checkCollisions(ctx, playerRectG)
}
}
for i, rect := range world.World.DestructibleRects {
if playerRectX.Overlaps(rect) {
collideX = i
s.checkCollisions(ctx, playerRectX)
}
if playerRectY.Overlaps(rect) {
collideY = i
s.checkCollisions(ctx, playerRectY)
}
if playerRectXY.Overlaps(rect) {
collideXY = i
s.checkCollisions(ctx, playerRectXY)
}
if playerRectG.Overlaps(rect) {
collideG = i
s.checkCollisions(ctx, 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
if s.OnGround != -1 || s.OnLadder != -1 {
// Reset jump counter.
if world.World.Jumps != 0 && time.Since(s.LastJump) >= 50*time.Millisecond {
world.World.Jumps = 0
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
}
// Reset dash counter.
if world.World.Dashes != 0 {
world.World.Dashes = 0
s.OnGround = collideG
if s.OnGround != -1 || s.OnLadder != -1 {
// Reset jump counter.
if world.World.Jumps != 0 && time.Since(s.LastJump) >= 50*time.Millisecond {
world.World.Jumps = 0
}
// Reset dash counter.
if world.World.Dashes != 0 {
world.World.Dashes = 0
}
}
}
// Remember positions to support rewinding time.
const recordTicks = 144 / 2
if ctx.Entity == world.World.Player {
if world.World.Rewinding {
s.playerPositionTicks = 0
} else {
s.playerPositionTicks++
if s.playerPositionTicks >= recordTicks || !s.recordedPosition {
s.RecordPosition(position)
// Remember positions to support rewinding time.
const recordTicks = 144 / 2
if ctx.Entity == world.World.Player {
if world.World.Rewinding {
s.playerPositionTicks = 0
s.recordedPosition = true
} else {
s.playerPositionTicks++
if s.playerPositionTicks >= recordTicks || !s.recordedPosition {
s.RecordPosition(position)
s.playerPositionTicks = 0
s.recordedPosition = true
}
}
}
}
// TODO: Does this use enough memory to require pruning positions?
// TODO: Does this use enough memory to require pruning positions?
// Update debug rects.
// Update debug rects.
if s.OnGround != lastOnGround || s.OnLadder != lastOnLadder {
s.UpdateDebugCollisionRects()
}
if s.OnGround != lastOnGround || s.OnLadder != lastOnLadder {
s.UpdateDebugCollisionRects()
}
} else {
position.X, position.Y = position.X+vx, position.Y+vy
bulletOffsetX := 0.0
bulletOffsetY := 1.0
bulletWidth := 1.0
bulletRectSmall := image.Rect(int(position.X+bulletOffsetX), int(position.Y+bulletOffsetY), int(position.X+bulletOffsetX+bulletWidth), int(position.Y+bulletOffsetY+bulletWidth))
bulletWidth = 5.0
bulletRectLarge := image.Rect(int(position.X+bulletOffsetX), int(position.Y+bulletOffsetY), int(position.X+bulletOffsetX+bulletWidth), int(position.Y+bulletOffsetY+bulletWidth))
for i, r := range world.World.DestructibleRects {
if r.Overlaps(bulletRectSmall) {
// Hit destructible.
ctx.RemoveEntity()
engine.Engine.RemoveEntity(world.World.DestructibleEntities[i])
world.World.DestructibleRects = append(world.World.DestructibleRects[:i], world.World.DestructibleRects[i+1:]...)
world.World.DestructibleEntities = append(world.World.DestructibleEntities[:i], world.World.DestructibleEntities[i+1:]...)
return nil
}
}
for _, r := range s.collisionRects {
if r.Overlaps(bulletRectSmall) {
// Hit wall.
ctx.RemoveEntity()
return nil
}
}
_ = bulletRectLarge
}
return nil
}

18
system/render.go

@ -5,6 +5,7 @@ import (
"time"
"code.rocketnine.space/tslocum/gohan"
"code.rocketnine.space/tslocum/monovania/asset"
"code.rocketnine.space/tslocum/monovania/component"
"code.rocketnine.space/tslocum/monovania/engine"
"code.rocketnine.space/tslocum/monovania/world"
@ -130,7 +131,7 @@ func (s *RenderSystem) Draw(ctx *gohan.Context, screen *ebiten.Image) error {
position := component.Position(ctx)
sprite := component.Sprite(ctx)
if sprite.NumFrames > 0 && time.Since(sprite.LastFrame) > sprite.FrameTime {
if sprite.NumFrames > 0 && !world.World.MessageVisible && time.Since(sprite.LastFrame) > sprite.FrameTime {
sprite.Frame++
if sprite.Frame >= sprite.NumFrames {
sprite.Frame = 0
@ -144,8 +145,17 @@ func (s *RenderSystem) Draw(ctx *gohan.Context, screen *ebiten.Image) error {
colorScale = sprite.ColorScale
}
// TODO
var drawn int
drawn += s.renderSprite(position.X+world.World.OffsetX, position.Y+world.World.OffsetY, 0, 0, 0, 1.0, colorScale, 1.0, sprite.HorizontalFlip, sprite.VerticalFlip, sprite.Image, screen)
s.renderSprite(position.X+world.World.OffsetX, position.Y+world.World.OffsetY, 0, 0, 0, 1.0, colorScale, 1.0, sprite.HorizontalFlip, sprite.VerticalFlip, sprite.Image, screen)
if sprite.Overlay != nil {
offsetX := sprite.OverlayX
if sprite.HorizontalFlip {
offsetX *= -1
}
offsetY := sprite.OverlayY
if sprite.Image == asset.PlayerSS.WalkR1 || sprite.Image == asset.PlayerSS.WalkR2 {
offsetY -= 1
}
s.renderSprite(position.X+world.World.OffsetX+offsetX, position.Y+world.World.OffsetY+offsetY, 0, 0, 0, 1.0, colorScale, 1.0, sprite.HorizontalFlip, sprite.VerticalFlip, sprite.Overlay, screen)
}
return nil
}

97
system/rendermessage.go

@ -6,28 +6,29 @@ import (
_ "image/png"
"strings"
"golang.org/x/image/colornames"
"code.rocketnine.space/tslocum/gohan"
"code.rocketnine.space/tslocum/monovania/component"
"code.rocketnine.space/tslocum/monovania/world"
"github.com/hajimehoshi/ebiten/v2"
"github.com/hajimehoshi/ebiten/v2/ebitenutil"
"golang.org/x/image/colornames"
)
type RenderMessageSystem struct {
player gohan.Entity
op *ebiten.DrawImageOptions
logoImg *ebiten.Image
debugImg *ebiten.Image
player gohan.Entity
op *ebiten.DrawImageOptions
logoImg *ebiten.Image
msgImg *ebiten.Image
tmpImg *ebiten.Image
}
func NewRenderMessageSystem(player gohan.Entity) *RenderMessageSystem {
s := &RenderMessageSystem{
player: player,
op: &ebiten.DrawImageOptions{},
logoImg: ebiten.NewImage(1, 1),
debugImg: ebiten.NewImage(200, 200),
player: player,
op: &ebiten.DrawImageOptions{},
logoImg: ebiten.NewImage(1, 1),
msgImg: ebiten.NewImage(1, 1),
tmpImg: ebiten.NewImage(200, 200),
}
return s
@ -50,8 +51,31 @@ func (s *RenderMessageSystem) Update(_ *gohan.Context) error {
}
func (s *RenderMessageSystem) Draw(ctx *gohan.Context, screen *ebiten.Image) error {
if world.World.GameOver {
// Draw game over screen.
if ctx.Entity == world.World.Player {
screen.Fill(colornames.Darkred)
}
return nil
}
if !world.World.MessageVisible {
return nil
}
// Draw message.
if world.World.MessageUpdated {
s.drawMessage()
}
bounds := s.msgImg.Bounds()
x := (float64(world.World.ScreenW) / 2) - (float64(bounds.Dx()) / 2)
y := (float64(world.World.ScreenH) / 2) - float64(bounds.Dy()) - 8
s.op.GeoM.Reset()
s.op.GeoM.Translate(x, y)
screen.DrawImage(s.msgImg, s.op)
// Draw logo.
if !world.World.GameStarted || world.World.FadingIn {
// Draw logo.
if ctx.Entity == world.World.Player {
var alpha float64
if !world.World.GameStarted {
@ -66,33 +90,23 @@ func (s *RenderMessageSystem) Draw(ctx *gohan.Context, screen *ebiten.Image) err
if alpha > 1 {
alpha = 1
}
op := &ebiten.DrawImageOptions{}
s.op.GeoM.Reset()
if !world.World.GameStarted {
op.ColorM.ChangeHSV(0, 1, alpha)
s.op.ColorM.ChangeHSV(0, 1, alpha)
} else {
op.ColorM.Scale(1, 1, 1, alpha)
s.op.ColorM.Scale(1, 1, 1, alpha)
}
screen.DrawImage(s.logoImg, op)
}
if !world.World.GameStarted {
return nil
}
} else if world.World.GameOver {
// Draw game over screen.
if ctx.Entity == world.World.Player {
screen.Fill(colornames.Darkred)
screen.DrawImage(s.logoImg, s.op)
s.op.ColorM.Reset()
}
return nil
}
if !world.World.MessageVisible {
return nil
}
return nil
}
/*position := component.Position(ctx)
velocity := component.Velocity(ctx)*/
func (s *RenderMessageSystem) drawMessage() {
message := world.World.MessageText + "\n\n<ENTER> TO CONTINUE."
split := strings.Split(world.World.MessageText, "\n")
split := strings.Split(message, "\n")
width := 0
for _, line := range split {
lineSize := len(line) * 12
@ -103,19 +117,19 @@ func (s *RenderMessageSystem) Draw(ctx *gohan.Context, screen *ebiten.Image) err
height := len(split) * 32
const padding = 8
width, height = width+padding*2, height+padding*2
x, y := (world.World.ScreenW-width)/2, (world.World.ScreenH-height)/2
screen.SubImage(image.Rect(x-padding, y-padding, x+width+padding, y+height+padding)).(*ebiten.Image).Fill(color.Black)
s.msgImg = ebiten.NewImage(width, height)
s.msgImg.Fill(color.RGBA{17, 17, 17, 255})
s.debugImg.Clear()
s.tmpImg.Clear()
s.tmpImg = ebiten.NewImage(width*2, height*2)
s.op.GeoM.Reset()
s.op.GeoM.Scale(2, 2)
s.op.GeoM.Translate(float64(world.World.ScreenW-width)/2, float64(world.World.ScreenH-height)/2)
ebitenutil.DebugPrint(s.debugImg, world.World.MessageText)
screen.DrawImage(s.debugImg, s.op)
return nil
s.op.GeoM.Translate(float64(padding), float64(padding))
ebitenutil.DebugPrint(s.tmpImg, message)
s.msgImg.DrawImage(s.tmpImg, s.op)
s.op.ColorM.Reset()
}
func (s *RenderMessageSystem) drawLogo() {
@ -129,7 +143,7 @@ func (s *RenderMessageSystem) drawLogo() {
logoOffset := int(float64(logoSize) * (4.0 / 9.0))
tailWidth := int(float64(logoSize) * (5.0 / 9.0))
x := (world.World.ScreenW / 2) - (totalSize / 2)
y := (world.World.ScreenH / 2)
y := world.World.ScreenH / 2
for i := 0; i < 3; i++ {
offset := i * logoOffset
s.logoImg.SubImage(image.Rect(x+offset, y-offset, x+logoSize+offset, y+logoSize-offset)).(*ebiten.Image).Fill(logoColor)
@ -149,4 +163,5 @@ func (s *RenderMessageSystem) drawLogo() {
func (s *RenderMessageSystem) SizeUpdated() {
s.drawLogo()
s.drawMessage()
}

39
world/world.go

@ -66,13 +66,17 @@ type GameWorld struct {
Rewinding bool
MessageVisible bool
MessageUpdated bool
MessageTitle string
MessageText string
TriggerRects []image.Rectangle
TriggerEntities []gohan.Entity
TriggerRects []image.Rectangle
TriggerNames []string
DestructibleEntities []gohan.Entity
DestructibleRects []image.Rectangle
DisableEsc bool
}
@ -221,7 +225,7 @@ func LoadMap(filePath string) {
for _, grp := range World.ObjectGroups {
if grp.Name == "PLAYERSPAWN" {
for _, obj := range grp.Objects {
World.SpawnX, World.SpawnY = obj.X, obj.Y-1
World.SpawnX, World.SpawnY = obj.X, obj.Y-0.1
}
break
}
@ -232,7 +236,7 @@ func LoadMap(filePath string) {
}
if grp.Name == "TEMPSPAWN" {
for _, obj := range grp.Objects {
World.SpawnX, World.SpawnY = obj.X, obj.Y-1
World.SpawnX, World.SpawnY = obj.X, obj.Y-0.1
}
break
}
@ -244,10 +248,6 @@ func LoadMap(filePath string) {
for _, grp := range World.ObjectGroups {
if grp.Name == "TRIGGERS" {
for _, obj := range grp.Objects {
if obj.Name == "" {
continue
}
mapTile := engine.Engine.NewEntity()
engine.Engine.AddComponent(mapTile, &component.PositionComponent{
X: obj.X,
@ -261,7 +261,24 @@ func LoadMap(filePath string) {
World.TriggerEntities = append(World.TriggerEntities, mapTile)
World.TriggerRects = append(World.TriggerRects, ObjectToRect(obj))
}
break
} else if grp.Name == "DESTRUCTIBLE" {
continue // TODO Fix destructible environment rects
for _, obj := range grp.Objects {
mapTile := engine.Engine.NewEntity()
engine.Engine.AddComponent(mapTile, &component.PositionComponent{
X: obj.X,
Y: obj.Y - 16,
})
engine.Engine.AddComponent(mapTile, &component.SpriteComponent{
Image: tileCache[obj.GID],
})
engine.Engine.AddComponent(mapTile, &component.DestructibleComponent{})
World.DestructibleEntities = append(World.DestructibleEntities, mapTile)
r := image.Rect(int(obj.X), int(obj.Y-32), int(obj.X+16), int(obj.Y-16))
World.DestructibleRects = append(World.DestructibleRects, r)
}
}
}
}
@ -270,3 +287,9 @@ func ObjectToRect(o *tiled.Object) image.Rectangle {
x, y, w, h := int(o.X), int(o.Y), int(o.Width), int(o.Height)
return image.Rect(x, y, x+w, y+h)
}
func (w *GameWorld) SetMessage(message string) {
w.MessageText = message
w.MessageVisible = true
w.MessageUpdated = true
}

Loading…
Cancel
Save