Add character animation and parallax background
|
@ -2,6 +2,7 @@ package asset
|
|||
|
||||
import (
|
||||
"embed"
|
||||
"image"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
)
|
||||
|
@ -10,3 +11,23 @@ var ImgWhiteSquare = ebiten.NewImage(16, 16)
|
|||
|
||||
//go:embed image map
|
||||
var FS embed.FS
|
||||
|
||||
var ImgBackground1 = LoadImage("image/szadiart-caves/background1.png")
|
||||
var ImgBackground2 = LoadImage("image/szadiart-caves/background2.png")
|
||||
var ImgBackground3 = LoadImage("image/szadiart-caves/background3.png")
|
||||
var ImgBackground4 = LoadImage("image/szadiart-caves/background4b.png")
|
||||
|
||||
func LoadImage(p string) *ebiten.Image {
|
||||
f, err := FS.Open(p)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
baseImg, _, err := image.Decode(f)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return ebiten.NewImageFromImage(baseImg)
|
||||
}
|
||||
|
|
Before Width: | Height: | Size: 15 KiB After Width: | Height: | Size: 13 KiB |
|
@ -1,5 +1,5 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<tileset version="1.5" tiledversion="1.7.2" name="tileset" tilewidth="16" tileheight="16" tilecount="64" columns="8">
|
||||
<tileset version="1.5" tiledversion="1.7.2" name="DONOTUSE-tileset" tilewidth="16" tileheight="16" tilecount="64" columns="8">
|
||||
<image source="tileset.png" width="128" height="128"/>
|
||||
<tile id="0">
|
||||
<objectgroup draworder="index" id="2">
|
||||
|
|
After Width: | Height: | Size: 12 KiB |
After Width: | Height: | Size: 14 KiB |
After Width: | Height: | Size: 45 KiB |
After Width: | Height: | Size: 66 KiB |
After Width: | Height: | Size: 72 KiB |
After Width: | Height: | Size: 101 KiB |
After Width: | Height: | Size: 156 KiB |
After Width: | Height: | Size: 123 KiB |
2689
asset/map/m1.tmx
|
@ -11,8 +11,12 @@ var PlayerSS = LoadPlayerSpriteSheet()
|
|||
|
||||
// PlayerSpriteSheet represents a collection of sprite images.
|
||||
type PlayerSpriteSheet struct {
|
||||
Frame1 *ebiten.Image
|
||||
Frame2 *ebiten.Image
|
||||
IdleR *ebiten.Image
|
||||
WalkR1 *ebiten.Image
|
||||
WalkR2 *ebiten.Image
|
||||
IdleL *ebiten.Image
|
||||
WalkL1 *ebiten.Image
|
||||
WalkL2 *ebiten.Image
|
||||
}
|
||||
|
||||
// LoadPlayerSpriteSheet loads the embedded PlayerSpriteSheet.
|
||||
|
@ -37,8 +41,12 @@ func LoadPlayerSpriteSheet() *PlayerSpriteSheet {
|
|||
|
||||
// Populate PlayerSpriteSheet.
|
||||
s := &PlayerSpriteSheet{}
|
||||
s.Frame1 = spriteAt(0, 0)
|
||||
s.Frame2 = spriteAt(0, 1)
|
||||
s.IdleR = spriteAt(0, 0)
|
||||
s.WalkR1 = spriteAt(1, 0)
|
||||
s.WalkR2 = spriteAt(2, 0)
|
||||
s.IdleL = spriteAt(0, 1)
|
||||
s.WalkL1 = spriteAt(1, 1)
|
||||
s.WalkL2 = spriteAt(2, 1)
|
||||
|
||||
return s
|
||||
}
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
package component
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"code.rocketnine.space/tslocum/gohan"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
)
|
||||
|
@ -10,6 +12,15 @@ type SpriteComponent struct {
|
|||
HorizontalFlip bool
|
||||
VerticalFlip bool
|
||||
DiagonalFlip bool // TODO unimplemented
|
||||
|
||||
Frame int
|
||||
Frames []*ebiten.Image
|
||||
FrameTime time.Duration
|
||||
LastFrame time.Time
|
||||
NumFrames int
|
||||
|
||||
OverrideColorScale bool
|
||||
ColorScale float64
|
||||
}
|
||||
|
||||
var SpriteComponentID = gohan.NewComponentID()
|
||||
|
|
|
@ -28,7 +28,7 @@ func NewPlayer(x, y float64) gohan.Entity {
|
|||
player.AddComponent(weapon)
|
||||
|
||||
player.AddComponent(&component.SpriteComponent{
|
||||
Image: asset.PlayerSS.Frame1,
|
||||
Image: asset.PlayerSS.IdleR,
|
||||
})
|
||||
|
||||
return player
|
||||
|
|
23
game/game.go
|
@ -82,22 +82,10 @@ func NewGame() (*game, error) {
|
|||
g.audioContext = audio.NewContext(sampleRate)
|
||||
|
||||
// TODO replace with fs embed
|
||||
g.changeMap("/home/trevor/programming/monovania/asset/map/m1.tmx")
|
||||
g.changeMap("map/m1.tmx")
|
||||
|
||||
spawnX, spawnY := -math.MaxFloat64, -math.MaxFloat64
|
||||
for _, grp := range world.World.ObjectGroups {
|
||||
if grp.Name == "PLAYERSPAWN" {
|
||||
for _, obj := range grp.Objects {
|
||||
spawnX, spawnY = obj.X, obj.Y-1
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if spawnX == -math.MaxFloat64 || spawnY == -math.MaxFloat64 {
|
||||
panic("world does not contain a player spawn object")
|
||||
}
|
||||
|
||||
g.player = entity.NewPlayer(spawnX, spawnY)
|
||||
world.World.Player = entity.NewPlayer(world.World.SpawnX, world.World.SpawnY)
|
||||
g.player = world.World.Player
|
||||
|
||||
g.addSystems()
|
||||
|
||||
|
@ -124,6 +112,7 @@ func (g *game) Layout(outsideWidth, outsideHeight int) (int, int) {
|
|||
s := ebiten.DeviceScaleFactor()
|
||||
w, h := int(s*float64(outsideWidth)), int(s*float64(outsideHeight))
|
||||
if w != g.w || h != g.h {
|
||||
world.World.ScreenW, world.World.ScreenH = w, h
|
||||
g.w, g.h = w, h
|
||||
g.movementSystem.ScreenW, g.movementSystem.ScreenH = float64(w), float64(h)
|
||||
g.renderSystem.ScreenW, g.renderSystem.ScreenH = w, h
|
||||
|
@ -164,10 +153,14 @@ func (g *game) addSystems() {
|
|||
|
||||
gohan.AddSystem(system.NewFireWeaponSystem(g.player))
|
||||
|
||||
gohan.AddSystem(system.NewRenderBackgroundSystem())
|
||||
|
||||
g.renderSystem = system.NewRenderSystem()
|
||||
gohan.AddSystem(g.renderSystem)
|
||||
|
||||
gohan.AddSystem(system.NewRenderDebugTextSystem(g.player))
|
||||
|
||||
gohan.AddSystem(system.NewProfileSystem(g.player))
|
||||
}
|
||||
|
||||
func (g *game) loadAssets() error {
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
package system
|
||||
|
||||
import (
|
||||
_ "image/png"
|
||||
|
||||
"code.rocketnine.space/tslocum/monovania/component"
|
||||
|
||||
"code.rocketnine.space/tslocum/monovania/asset"
|
||||
|
||||
"code.rocketnine.space/tslocum/monovania/world"
|
||||
|
||||
"code.rocketnine.space/tslocum/gohan"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
)
|
||||
|
||||
type RenderBackgroundSystem struct {
|
||||
op *ebiten.DrawImageOptions
|
||||
}
|
||||
|
||||
func NewRenderBackgroundSystem() *RenderBackgroundSystem {
|
||||
s := &RenderBackgroundSystem{
|
||||
op: &ebiten.DrawImageOptions{},
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *RenderBackgroundSystem) Matches(entity gohan.Entity) bool {
|
||||
return entity == world.World.Player
|
||||
}
|
||||
|
||||
func (s *RenderBackgroundSystem) Update(_ gohan.Entity) error {
|
||||
return gohan.ErrSystemWithoutUpdate
|
||||
}
|
||||
|
||||
func (s *RenderBackgroundSystem) Draw(entity gohan.Entity, screen *ebiten.Image) error {
|
||||
if world.World.GameOver {
|
||||
return nil
|
||||
}
|
||||
|
||||
position := component.Position(world.World.Player)
|
||||
|
||||
pctX, pctY := position.X/(512*16), position.Y/(512*16)
|
||||
|
||||
scale := (float64(world.World.ScreenH) / float64(asset.ImgBackground1.Bounds().Dy())) * 1.7
|
||||
offsetX, offsetY := float64(asset.ImgBackground1.Bounds().Dx())*pctX, float64(asset.ImgBackground1.Bounds().Dy())*pctY
|
||||
|
||||
op := &ebiten.DrawImageOptions{}
|
||||
op.GeoM.Scale(scale, scale)
|
||||
screen.DrawImage(asset.ImgBackground1, op)
|
||||
op = &ebiten.DrawImageOptions{}
|
||||
op.GeoM.Scale(scale, scale)
|
||||
op.GeoM.Translate(-offsetX*0.5, -offsetY*0.5)
|
||||
screen.DrawImage(asset.ImgBackground2, op)
|
||||
op = &ebiten.DrawImageOptions{}
|
||||
op.GeoM.Scale(scale, scale)
|
||||
op.GeoM.Translate(-offsetX*1, -offsetY*0.75)
|
||||
screen.DrawImage(asset.ImgBackground3, op)
|
||||
op = &ebiten.DrawImageOptions{}
|
||||
op.GeoM.Scale(scale, scale)
|
||||
op.GeoM.Translate(-offsetX*2, -offsetY)
|
||||
screen.DrawImage(asset.ImgBackground4, op)
|
||||
return nil
|
||||
}
|
|
@ -47,6 +47,8 @@ func (s *fireWeaponSystem) fire(weapon *component.WeaponComponent, position *com
|
|||
}
|
||||
|
||||
func (s *fireWeaponSystem) Update(_ gohan.Entity) error {
|
||||
return nil // TODO
|
||||
|
||||
weapon := component.Weapon(s.player)
|
||||
|
||||
if weapon.Ammo <= 0 {
|
||||
|
|
|
@ -3,6 +3,8 @@ package system
|
|||
import (
|
||||
"time"
|
||||
|
||||
"code.rocketnine.space/tslocum/monovania/asset"
|
||||
|
||||
"code.rocketnine.space/tslocum/monovania/world"
|
||||
|
||||
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
||||
|
@ -13,8 +15,9 @@ import (
|
|||
)
|
||||
|
||||
type playerMoveSystem struct {
|
||||
player gohan.Entity
|
||||
movement *MovementSystem
|
||||
player gohan.Entity
|
||||
movement *MovementSystem
|
||||
lastWalkDirL bool
|
||||
}
|
||||
|
||||
func NewPlayerMoveSystem(player gohan.Entity, m *MovementSystem) *playerMoveSystem {
|
||||
|
@ -34,7 +37,11 @@ func (s *playerMoveSystem) Update(e gohan.Entity) error {
|
|||
if world.World.Debug > 2 {
|
||||
world.World.Debug = 0
|
||||
}
|
||||
s.movement.UpdateDrawnRects()
|
||||
s.movement.UpdateDebugCollisionRects()
|
||||
return nil
|
||||
}
|
||||
if ebiten.IsKeyPressed(ebiten.KeyControl) && inpututil.IsKeyJustPressed(ebiten.KeyN) {
|
||||
world.World.NoClip = !world.World.NoClip
|
||||
return nil
|
||||
}
|
||||
|
||||
|
@ -43,6 +50,13 @@ func (s *playerMoveSystem) Update(e gohan.Entity) error {
|
|||
maxYSpeed := 0.5
|
||||
const jumpVelocity = -0.75
|
||||
|
||||
if world.World.Debug > 0 && ebiten.IsKeyPressed(ebiten.KeyShift) {
|
||||
maxSpeed *= 10
|
||||
maxYSpeed = 40
|
||||
}
|
||||
|
||||
var walkKeyPressed bool
|
||||
|
||||
velocity := component.Velocity(s.player)
|
||||
if ebiten.IsKeyPressed(ebiten.KeyA) {
|
||||
if velocity.X > -maxSpeed {
|
||||
|
@ -52,6 +66,19 @@ func (s *playerMoveSystem) Update(e gohan.Entity) error {
|
|||
velocity.X -= moveSpeed
|
||||
}
|
||||
}
|
||||
|
||||
sprite := component.Sprite(s.player)
|
||||
sprite.Frames = []*ebiten.Image{
|
||||
asset.PlayerSS.WalkL1,
|
||||
asset.PlayerSS.IdleL,
|
||||
asset.PlayerSS.WalkL2,
|
||||
asset.PlayerSS.IdleL,
|
||||
}
|
||||
sprite.NumFrames = 4
|
||||
sprite.FrameTime = 150 * time.Millisecond
|
||||
|
||||
walkKeyPressed = true
|
||||
s.lastWalkDirL = true
|
||||
}
|
||||
if ebiten.IsKeyPressed(ebiten.KeyD) {
|
||||
if velocity.X < maxSpeed {
|
||||
|
@ -61,17 +88,56 @@ func (s *playerMoveSystem) Update(e gohan.Entity) error {
|
|||
velocity.X += moveSpeed
|
||||
}
|
||||
}
|
||||
|
||||
sprite := component.Sprite(s.player)
|
||||
sprite.Frames = []*ebiten.Image{
|
||||
asset.PlayerSS.WalkR1,
|
||||
asset.PlayerSS.IdleR,
|
||||
asset.PlayerSS.WalkR2,
|
||||
asset.PlayerSS.IdleR,
|
||||
}
|
||||
sprite.NumFrames = 4
|
||||
sprite.FrameTime = 150 * time.Millisecond
|
||||
|
||||
walkKeyPressed = true
|
||||
s.lastWalkDirL = false
|
||||
}
|
||||
if s.movement.OnLadder != -1 {
|
||||
if s.movement.OnLadder != -1 || world.World.NoClip {
|
||||
setLadderFrames := func() {
|
||||
sprite := component.Sprite(s.player)
|
||||
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.NumFrames = 4
|
||||
sprite.FrameTime = 150 * time.Millisecond
|
||||
}
|
||||
if ebiten.IsKeyPressed(ebiten.KeyW) {
|
||||
if velocity.Y > -maxYSpeed {
|
||||
velocity.Y -= moveSpeed
|
||||
}
|
||||
|
||||
setLadderFrames()
|
||||
walkKeyPressed = true
|
||||
}
|
||||
if ebiten.IsKeyPressed(ebiten.KeyS) {
|
||||
if ebiten.IsKeyPressed(ebiten.KeyS) && s.movement.OnGround == -1 {
|
||||
if velocity.Y < maxYSpeed {
|
||||
velocity.Y += moveSpeed
|
||||
}
|
||||
|
||||
setLadderFrames()
|
||||
walkKeyPressed = true
|
||||
}
|
||||
} else {
|
||||
// Jump.
|
||||
|
@ -86,6 +152,24 @@ func (s *playerMoveSystem) Update(e gohan.Entity) error {
|
|||
}
|
||||
}
|
||||
|
||||
if !walkKeyPressed || (s.movement.OnGround == -1 && s.movement.OnLadder == -1) {
|
||||
sprite := component.Sprite(s.player)
|
||||
sprite.NumFrames = 0
|
||||
if s.lastWalkDirL {
|
||||
if s.movement.OnGround == -1 && s.movement.OnLadder == -1 {
|
||||
sprite.Image = asset.PlayerSS.WalkL2
|
||||
} else {
|
||||
sprite.Image = asset.PlayerSS.IdleL
|
||||
}
|
||||
} else {
|
||||
if s.movement.OnGround == -1 && s.movement.OnLadder == -1 {
|
||||
sprite.Image = asset.PlayerSS.WalkR2
|
||||
} else {
|
||||
sprite.Image = asset.PlayerSS.IdleR
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,65 @@
|
|||
package system
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"path"
|
||||
"runtime"
|
||||
"runtime/pprof"
|
||||
|
||||
"code.rocketnine.space/tslocum/gohan"
|
||||
"github.com/hajimehoshi/ebiten/v2"
|
||||
"github.com/hajimehoshi/ebiten/v2/inpututil"
|
||||
)
|
||||
|
||||
type profileSystem struct {
|
||||
player gohan.Entity
|
||||
cpuProfile *os.File
|
||||
}
|
||||
|
||||
func NewProfileSystem(player gohan.Entity) *profileSystem {
|
||||
return &profileSystem{
|
||||
player: player,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *profileSystem) Matches(e gohan.Entity) bool {
|
||||
return e == s.player
|
||||
}
|
||||
|
||||
func (s *profileSystem) Update(e gohan.Entity) error {
|
||||
if ebiten.IsKeyPressed(ebiten.KeyControl) && inpututil.IsKeyJustPressed(ebiten.KeyP) {
|
||||
if s.cpuProfile == nil {
|
||||
log.Println("CPU profiling started...")
|
||||
|
||||
runtime.SetCPUProfileRate(1000)
|
||||
|
||||
homeDir, err := os.UserHomeDir()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.cpuProfile, err = os.Create(path.Join(homeDir, "monovania.prof"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = pprof.StartCPUProfile(s.cpuProfile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
pprof.StopCPUProfile()
|
||||
|
||||
s.cpuProfile.Close()
|
||||
s.cpuProfile = nil
|
||||
|
||||
log.Println("CPU profiling stopped")
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *profileSystem) Draw(_ gohan.Entity, _ *ebiten.Image) error {
|
||||
return gohan.ErrSystemWithoutDraw
|
||||
}
|
|
@ -3,8 +3,6 @@ package system
|
|||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"code.rocketnine.space/tslocum/monovania/world"
|
||||
|
@ -24,54 +22,79 @@ type MovementSystem struct {
|
|||
Jumping bool
|
||||
LastJump time.Time
|
||||
|
||||
collisionIndex int
|
||||
collisionRects []image.Rectangle
|
||||
|
||||
ladderRects []image.Rectangle
|
||||
|
||||
debugRects []gohan.Entity
|
||||
fireRects []image.Rectangle
|
||||
|
||||
debugCollisionRects []gohan.Entity
|
||||
debugLadderRects []gohan.Entity
|
||||
}
|
||||
|
||||
func NewMovementSystem() *MovementSystem {
|
||||
s := &MovementSystem{
|
||||
collisionIndex: -1,
|
||||
OnGround: -1,
|
||||
OnLadder: -1,
|
||||
OnGround: -1,
|
||||
OnLadder: -1,
|
||||
}
|
||||
|
||||
w := world.World
|
||||
|
||||
// Cache collision rects.
|
||||
|
||||
for i, objectLayer := range w.ObjectGroups {
|
||||
if strings.ToLower(objectLayer.Name) == "collisions" {
|
||||
s.collisionIndex = i
|
||||
break
|
||||
m := w.Map
|
||||
for _, layer := range m.Layers {
|
||||
collision := layer.Properties.GetBool("collision")
|
||||
if !collision {
|
||||
continue
|
||||
}
|
||||
}
|
||||
if s.collisionIndex == -1 {
|
||||
log.Fatal("no collisions")
|
||||
return s
|
||||
}
|
||||
|
||||
for _, object := range w.ObjectGroups[s.collisionIndex].Objects {
|
||||
rect := objectToRect(object)
|
||||
s.collisionRects = append(s.collisionRects, rect)
|
||||
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.collisionRects = append(s.collisionRects, image.Rect(int(gx), int(gy), int(gx)+16, int(gy)+16))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cache ladder rects.
|
||||
|
||||
for _, objectLayer := range w.ObjectGroups {
|
||||
if strings.ToLower(objectLayer.Name) == "ladders" {
|
||||
for _, object := range objectLayer.Objects {
|
||||
rect := objectToRect(object)
|
||||
s.ladderRects = append(s.ladderRects, rect)
|
||||
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))
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
s.UpdateDrawnRects()
|
||||
// Cache fire rects.
|
||||
|
||||
for _, layer := range world.World.ObjectGroups {
|
||||
if layer.Name != "FIRES" {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, obj := range layer.Objects {
|
||||
rect := objectToRect(obj)
|
||||
s.fireRects = append(s.fireRects, rect)
|
||||
}
|
||||
}
|
||||
|
||||
s.UpdateDebugCollisionRects()
|
||||
|
||||
return s
|
||||
}
|
||||
|
@ -93,40 +116,66 @@ func drawDebugRect(r image.Rectangle, c color.Color) gohan.Entity {
|
|||
})
|
||||
|
||||
rectEntity.AddComponent(&component.SpriteComponent{
|
||||
Image: rectImg,
|
||||
Image: rectImg,
|
||||
OverrideColorScale: true,
|
||||
})
|
||||
|
||||
return rectEntity
|
||||
}
|
||||
|
||||
func (s *MovementSystem) UpdateDrawnRects() {
|
||||
for _, e := range s.debugRects {
|
||||
func (s *MovementSystem) removeDebugRects() {
|
||||
for _, e := range s.debugCollisionRects {
|
||||
e.Remove()
|
||||
}
|
||||
s.debugCollisionRects = nil
|
||||
|
||||
for _, e := range s.debugLadderRects {
|
||||
e.Remove()
|
||||
}
|
||||
s.debugLadderRects = nil
|
||||
}
|
||||
|
||||
func (s *MovementSystem) addDebugCollisionRects() {
|
||||
s.removeDebugRects()
|
||||
|
||||
for _, rect := range s.collisionRects {
|
||||
c := color.RGBA{200, 200, 200, 150}
|
||||
debugRect := drawDebugRect(rect, c)
|
||||
s.debugCollisionRects = append(s.debugCollisionRects, debugRect)
|
||||
}
|
||||
|
||||
for _, rect := range s.ladderRects {
|
||||
c := color.RGBA{200, 200, 200, 150}
|
||||
debugRect := drawDebugRect(rect, c)
|
||||
s.debugLadderRects = append(s.debugLadderRects, debugRect)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MovementSystem) UpdateDebugCollisionRects() {
|
||||
if world.World.Debug < 2 {
|
||||
s.removeDebugRects()
|
||||
return
|
||||
} else if len(s.debugCollisionRects) == 0 {
|
||||
s.addDebugCollisionRects()
|
||||
}
|
||||
|
||||
collideColor := color.RGBA{255, 255, 255, 200}
|
||||
|
||||
for i, rect := range s.collisionRects {
|
||||
c := color.RGBA{200, 200, 200, 80}
|
||||
for i, debugRect := range s.debugCollisionRects {
|
||||
sprite := component.Sprite(debugRect)
|
||||
if s.OnGround == i {
|
||||
c = collideColor
|
||||
sprite.ColorScale = 1
|
||||
} else {
|
||||
sprite.ColorScale = 0.4
|
||||
}
|
||||
debugRect := drawDebugRect(rect, c)
|
||||
s.debugRects = append(s.debugRects, debugRect)
|
||||
}
|
||||
|
||||
for i, rect := range s.ladderRects {
|
||||
c := color.RGBA{200, 200, 200, 80}
|
||||
for i, debugRect := range s.debugLadderRects {
|
||||
sprite := component.Sprite(debugRect)
|
||||
if s.OnLadder == i {
|
||||
c = collideColor
|
||||
sprite.ColorScale = 1
|
||||
} else {
|
||||
sprite.ColorScale = 0.4
|
||||
}
|
||||
debugRect := drawDebugRect(rect, c)
|
||||
s.debugRects = append(s.debugRects, debugRect)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (s *MovementSystem) objectToGameCoords(x, y, height float64) (float64, float64) {
|
||||
|
@ -140,6 +189,20 @@ func (_ *MovementSystem) Matches(entity gohan.Entity) bool {
|
|||
return position != nil && velocity != nil
|
||||
}
|
||||
|
||||
func (s *MovementSystem) checkFire(r image.Rectangle) {
|
||||
for _, fireRect := range s.fireRects {
|
||||
if r.Overlaps(fireRect) {
|
||||
//world.World.GameOver = true
|
||||
// TODO
|
||||
position := component.Position(world.World.Player)
|
||||
velocity := component.Velocity(world.World.Player)
|
||||
position.X, position.Y = world.World.SpawnX, world.World.SpawnY
|
||||
velocity.X, velocity.Y = 0, 0
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *MovementSystem) Update(entity gohan.Entity) error {
|
||||
lastOnGround := s.OnGround
|
||||
lastOnLadder := s.OnLadder
|
||||
|
@ -171,7 +234,10 @@ func (s *MovementSystem) Update(entity gohan.Entity) error {
|
|||
const maxGravity = 9
|
||||
const gravityAccel = 0.04
|
||||
if bullet == nil {
|
||||
if s.OnLadder != -1 {
|
||||
if s.OnLadder != -1 || world.World.NoClip {
|
||||
velocity.X *= decel
|
||||
velocity.Y *= decel
|
||||
} else if s.OnLadder != -1 {
|
||||
velocity.X *= decel
|
||||
velocity.Y *= ladderDecel
|
||||
} else if velocity.Y < maxGravity {
|
||||
|
@ -197,17 +263,24 @@ func (s *MovementSystem) Update(entity gohan.Entity) error {
|
|||
playerRectXY := image.Rect(int(position.X+velocity.X), int(position.Y+velocity.Y), int(position.X+velocity.X)+16, int(position.Y+velocity.Y)+17)
|
||||
playerRectG := image.Rect(int(position.X), int(position.Y+threshold), int(position.X)+16, int(position.Y+threshold)+17)
|
||||
for i, rect := range s.collisionRects {
|
||||
if world.World.NoClip {
|
||||
continue
|
||||
}
|
||||
if playerRectX.Overlaps(rect) {
|
||||
collideX = i
|
||||
s.checkFire(playerRectX)
|
||||
}
|
||||
if playerRectY.Overlaps(rect) {
|
||||
collideY = i
|
||||
s.checkFire(playerRectY)
|
||||
}
|
||||
if playerRectXY.Overlaps(rect) {
|
||||
collideXY = i
|
||||
s.checkFire(playerRectXY)
|
||||
}
|
||||
if playerRectG.Overlaps(rect) {
|
||||
collideG = i
|
||||
s.checkFire(playerRectG)
|
||||
}
|
||||
}
|
||||
if collideXY == -1 {
|
||||
|
@ -226,7 +299,7 @@ func (s *MovementSystem) Update(entity gohan.Entity) error {
|
|||
// Update debug rects.
|
||||
|
||||
if s.OnGround != lastOnGround || s.OnLadder != lastOnLadder {
|
||||
s.UpdateDrawnRects()
|
||||
s.UpdateDebugCollisionRects()
|
||||
}
|
||||
|
||||
return nil
|
||||
|
|
|
@ -2,6 +2,11 @@ package system
|
|||
|
||||
import (
|
||||
_ "image/png"
|
||||
"time"
|
||||
|
||||
"golang.org/x/image/colornames"
|
||||
|
||||
"code.rocketnine.space/tslocum/monovania/world"
|
||||
|
||||
"code.rocketnine.space/tslocum/gohan"
|
||||
"code.rocketnine.space/tslocum/monovania/component"
|
||||
|
@ -98,7 +103,7 @@ func (s *RenderSystem) renderSprite(x float64, y float64, offsetx float64, offse
|
|||
s.op.ColorM.Scale(colorScale, colorScale, colorScale, alpha)
|
||||
|
||||
// Apply monochrome filter.
|
||||
s.op.ColorM.ChangeHSV(1, 0, 1)
|
||||
//s.op.ColorM.ChangeHSV(1, 0, 1)
|
||||
|
||||
target.DrawImage(sprite, s.op)
|
||||
|
||||
|
@ -108,10 +113,31 @@ func (s *RenderSystem) renderSprite(x float64, y float64, offsetx float64, offse
|
|||
}
|
||||
|
||||
func (s *RenderSystem) Draw(entity gohan.Entity, screen *ebiten.Image) error {
|
||||
if world.World.GameOver {
|
||||
if entity == world.World.Player {
|
||||
screen.Fill(colornames.Darkred)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
position := component.Position(entity)
|
||||
sprite := component.Sprite(entity)
|
||||
|
||||
if sprite.NumFrames > 0 && time.Since(sprite.LastFrame) > sprite.FrameTime {
|
||||
sprite.Frame++
|
||||
if sprite.Frame >= sprite.NumFrames {
|
||||
sprite.Frame = 0
|
||||
}
|
||||
sprite.Image = sprite.Frames[sprite.Frame]
|
||||
sprite.LastFrame = time.Now()
|
||||
}
|
||||
|
||||
colorScale := 1.0
|
||||
if sprite.OverrideColorScale {
|
||||
colorScale = sprite.ColorScale
|
||||
}
|
||||
|
||||
var drawn int
|
||||
drawn += s.renderSprite(position.X, position.Y, 0, 0, 0, 1.0, 1.0, 1.0, sprite.HorizontalFlip, sprite.VerticalFlip, sprite.Image, screen)
|
||||
drawn += s.renderSprite(position.X, position.Y, 0, 0, 0, 1.0, colorScale, 1.0, sprite.HorizontalFlip, sprite.VerticalFlip, sprite.Image, screen)
|
||||
return nil
|
||||
}
|
||||
|
|
116
world/world.go
|
@ -1,11 +1,15 @@
|
|||
package world
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"image"
|
||||
"io/ioutil"
|
||||
"math"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"code.rocketnine.space/tslocum/monovania/asset"
|
||||
|
||||
"code.rocketnine.space/tslocum/gohan"
|
||||
"code.rocketnine.space/tslocum/monovania/component"
|
||||
|
@ -13,22 +17,34 @@ import (
|
|||
"github.com/lafriks/go-tiled"
|
||||
)
|
||||
|
||||
var World = &GameWorld{}
|
||||
|
||||
type GameWorld struct {
|
||||
Map *tiled.Map
|
||||
ObjectGroups []*tiled.ObjectGroup
|
||||
Debug int
|
||||
var World = &GameWorld{
|
||||
StartedAt: time.Now(),
|
||||
}
|
||||
|
||||
func tileToGameCoords(x, y int) (float64, float64) {
|
||||
type GameWorld struct {
|
||||
Map *tiled.Map
|
||||
SpawnX, SpawnY float64
|
||||
ObjectGroups []*tiled.ObjectGroup
|
||||
StartedAt time.Time
|
||||
GameOver bool
|
||||
Player gohan.Entity
|
||||
ScreenW, ScreenH int
|
||||
NoClip bool
|
||||
Debug int
|
||||
}
|
||||
|
||||
func TileToGameCoords(x, y int) (float64, float64) {
|
||||
//return float64(x) * 16, float64(g.currentMap.Height*16) - float64(y)*16 - 16
|
||||
return float64(x) * 16, float64(y) * 16
|
||||
}
|
||||
|
||||
func LoadMap(filePath string) {
|
||||
loader := tiled.Loader{
|
||||
FileSystem: http.FS(asset.FS),
|
||||
}
|
||||
|
||||
// Parse .tmx file.
|
||||
m, err := tiled.LoadFromFile(filePath)
|
||||
m, err := loader.LoadFromFile(filePath)
|
||||
if err != nil {
|
||||
fmt.Printf("error parsing world: %s", err.Error())
|
||||
os.Exit(2)
|
||||
|
@ -38,13 +54,14 @@ func LoadMap(filePath string) {
|
|||
|
||||
tileset := m.Tilesets[0]
|
||||
|
||||
imgPath := tileset.GetFileFullPath(tileset.Image.Source)
|
||||
b, err := ioutil.ReadFile(imgPath)
|
||||
imgPath := filepath.Join("./map/", tileset.Image.Source)
|
||||
f, err := asset.FS.Open(imgPath)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
defer f.Close()
|
||||
|
||||
img, _, err := image.Decode(bytes.NewReader(b))
|
||||
img, _, err := image.Decode(f)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
@ -57,6 +74,25 @@ func LoadMap(filePath string) {
|
|||
rect := tileset.GetTileRect(i)
|
||||
tileCache[i+tileset.FirstGID] = tilesetImg.SubImage(rect).(*ebiten.Image)
|
||||
}
|
||||
|
||||
createTileEntity := func(t *tiled.LayerTile, x int, y int) gohan.Entity {
|
||||
tileX, tileY := TileToGameCoords(x, y)
|
||||
|
||||
mapTile := gohan.NewEntity()
|
||||
mapTile.AddComponent(&component.PositionComponent{
|
||||
X: tileX,
|
||||
Y: tileY,
|
||||
})
|
||||
mapTile.AddComponent(&component.SpriteComponent{
|
||||
Image: tileCache[t.Tileset.FirstGID+t.ID],
|
||||
HorizontalFlip: t.HorizontalFlip,
|
||||
VerticalFlip: t.VerticalFlip,
|
||||
DiagonalFlip: t.DiagonalFlip,
|
||||
})
|
||||
|
||||
return mapTile
|
||||
}
|
||||
|
||||
var t *tiled.LayerTile
|
||||
for _, layer := range m.Layers {
|
||||
for y := 0; y < m.Height; y++ {
|
||||
|
@ -74,19 +110,7 @@ func LoadMap(filePath string) {
|
|||
continue
|
||||
}
|
||||
|
||||
tileX, tileY := tileToGameCoords(x, y)
|
||||
|
||||
mapTile := gohan.NewEntity()
|
||||
mapTile.AddComponent(&component.PositionComponent{
|
||||
X: tileX,
|
||||
Y: tileY,
|
||||
})
|
||||
mapTile.AddComponent(&component.SpriteComponent{
|
||||
Image: tileImg,
|
||||
HorizontalFlip: t.HorizontalFlip,
|
||||
VerticalFlip: t.VerticalFlip,
|
||||
DiagonalFlip: t.DiagonalFlip,
|
||||
})
|
||||
_ = createTileEntity(t, x, y)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -112,4 +136,44 @@ func LoadMap(filePath string) {
|
|||
|
||||
World.Map = m
|
||||
World.ObjectGroups = objects
|
||||
|
||||
World.SpawnX, World.SpawnY = -math.MaxFloat64, -math.MaxFloat64
|
||||
for _, grp := range World.ObjectGroups {
|
||||
if grp.Name == "PLAYERSPAWN" {
|
||||
for _, obj := range grp.Objects {
|
||||
World.SpawnX, World.SpawnY = obj.X, obj.Y-1
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
for _, grp := range World.ObjectGroups {
|
||||
if !grp.Visible {
|
||||
continue
|
||||
}
|
||||
if grp.Name == "TEMPSPAWN" {
|
||||
for _, obj := range grp.Objects {
|
||||
World.SpawnX, World.SpawnY = obj.X, obj.Y-1
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
if World.SpawnX == -math.MaxFloat64 || World.SpawnY == -math.MaxFloat64 {
|
||||
panic("world does not contain a player spawn object")
|
||||
}
|
||||
|
||||
for _, grp := range World.ObjectGroups {
|
||||
if grp.Name == "TRIGGERS" {
|
||||
for _, obj := range grp.Objects {
|
||||
mapTile := gohan.NewEntity()
|
||||
mapTile.AddComponent(&component.PositionComponent{
|
||||
X: obj.X,
|
||||
Y: obj.Y - 16,
|
||||
})
|
||||
mapTile.AddComponent(&component.SpriteComponent{
|
||||
Image: tileCache[obj.GID],
|
||||
})
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|