diff --git a/.gitignore b/.gitignore index ca1f0e2..daabb01 100644 --- a/.gitignore +++ b/.gitignore @@ -5,3 +5,6 @@ __debug_bin rice-box.go *.rice-box.* + +# Application +cmd/krampus19/res/_sandbox/ diff --git a/cmd/krampus19/level.go b/cmd/krampus19/level.go index 2698c9e..b3530bf 100644 --- a/cmd/krampus19/level.go +++ b/cmd/krampus19/level.go @@ -4,27 +4,29 @@ import ( "errors" "fmt" "io" + + "opslag.de/schobers/geom" ) -type entity byte +type entityType byte type tile byte const ( - entityInvalid entity = entity(0) - entityNone = '_' - entityCharacter = '@' - entityVillain = 'X' - entityBrick = 'B' - entityCrate = 'C' + entityTypeInvalid entityType = entityType(0) + entityTypeNone = '_' + entityTypeCharacter = '@' + entityTypeVillain = 'X' + entityTypeBrick = 'B' + entityTypeCrate = 'C' ) -func (e entity) IsValid() bool { +func (e entityType) IsValid() bool { switch e { - case entityNone: - case entityCharacter: - case entityVillain: - case entityBrick: - case entityCrate: + case entityTypeNone: + case entityTypeCharacter: + case entityTypeVillain: + case entityTypeBrick: + case entityTypeCrate: default: return false } @@ -53,7 +55,16 @@ type level struct { width int height int tiles []tile - entities []entity + entities []entityType +} + +func (l level) idxToPos(i int) geom.Point { return geom.Pt(i%l.width, i/l.width) } + +func (l level) posToIdx(p geom.Point) int { + if p.X < 0 || p.Y < 0 || p.X >= l.width || p.Y >= l.height { + return -1 + } + return p.Y*l.width + p.X } func loadLevelAsset(r io.Reader) (level, error) { @@ -108,10 +119,10 @@ func (c *levelContext) parseRow(p *lineParser) parseLineFn { func (c *levelContext) addRow(p *lineParser, line string) parseLineFn { var tiles []tile - var entities []entity + var entities []entityType for i := 0; i < len(line); i += 2 { tiles = append(tiles, tile(line[i])) - entities = append(entities, entity(line[i+1])) + entities = append(entities, entityType(line[i+1])) } for i, t := range tiles { @@ -121,7 +132,7 @@ func (c *levelContext) addRow(p *lineParser, line string) parseLineFn { } for i, e := range entities { if !e.IsValid() { - return p.emitErr(fmt.Errorf("level contains invalid entity at (%d, %d)", i, c.level.height)) + return p.emitErr(fmt.Errorf("level contains invalid entity type at (%d, %d)", i, c.level.height)) } } diff --git a/cmd/krampus19/playlevel.go b/cmd/krampus19/playlevel.go index f17dba1..a211925 100644 --- a/cmd/krampus19/playlevel.go +++ b/cmd/krampus19/playlevel.go @@ -1,9 +1,13 @@ package main import ( + "sort" + "time" + "opslag.de/schobers/allg5" "opslag.de/schobers/geom" "opslag.de/schobers/krampus19/alui" + "opslag.de/schobers/krampus19/gut" ) type playLevel struct { @@ -14,16 +18,84 @@ type playLevel struct { scale float32 level level - player geom.PointF32 - moving bool + player *entity + bricks []*entity + sunken []*entity + steps int + + tick time.Duration + ani gut.Animations +} + +type entity struct { + typ entityType + pos geom.Point + scr geom.PointF32 +} + +func newEntity(typ entityType, pos geom.Point) *entity { + return &entity{typ, pos, pos.ToF32()} +} + +type posToScrFn func(geom.Point) geom.PointF32 + +type entityMoveAnimation struct { + e *entity + from, to geom.Point + pos geom.PointF32 +} + +func newEntityMoveAnimation(e *entity, to geom.Point) *entityMoveAnimation { + ani := &entityMoveAnimation{e: e, from: e.pos, to: to, pos: e.pos.ToF32()} + ani.e.pos = to + return ani +} + +func (a *entityMoveAnimation) Animate(start, now time.Duration) bool { + const duration = 240 * time.Millisecond + + progress := float32((now-start)*1000/duration) * .001 + from, to := a.from.ToF32(), a.to.ToF32() + if progress >= 1 { + a.e.scr = to + return false + } + a.e.scr = to.Sub(from).Mul(progress).Add(from) + return true +} + +func (s *playLevel) idxToPos(i int) geom.PointF32 { return s.level.idxToPos(i).ToF32() } + +func (s *playLevel) isIdle() bool { return s.ani.Idle() } + +func (s *playLevel) canMove(to geom.Point) bool { + idx := s.level.posToIdx(to) + if idx == -1 { + return false + } + return true } func (s *playLevel) loadLevel(name string) { s.level = s.ctx.Levels[name] + s.bricks = nil + s.sunken = nil + for i, e := range s.level.entities { + switch e { + case entityTypeCharacter: + s.player = newEntity(e, s.level.idxToPos(i)) + case entityTypeBrick: + s.bricks = append(s.bricks, newEntity(e, s.level.idxToPos(i))) + } + } } -func (s *playLevel) toScreenPos(p geom.Point) geom.PointF32 { - pos := geom.PtF32(float32(p.X)+.5, float32(p.Y)+.5) +func (s *playLevel) posToScreen(p geom.Point) geom.PointF32 { + return s.posToScreenF32(p.ToF32()) +} + +func (s *playLevel) posToScreenF32(p geom.PointF32) geom.PointF32 { + pos := p.Add2D(.5, .5) pos = geom.PtF32(pos.X*148-pos.Y*46, pos.Y*82) pos = geom.PtF32(pos.X*s.scale, pos.Y*s.scale) return pos.Add(s.offset) @@ -50,9 +122,34 @@ func (s *playLevel) Layout(ctx *alui.Context, bounds geom.RectangleF32) { tilesCenter = geom.PtF32(tilesCenter.X*s.scale, tilesCenter.Y*s.scale) center := bounds.Center() s.offset = geom.PtF32(center.X-tilesCenter.X, center.Y-tilesCenter.Y) + + s.ani.Animate(s.ctx.Tick) +} + +func (s *playLevel) tryPlayerMove(to geom.Point) { + if s.isIdle() && s.canMove(to) { + s.ani.Start(s.ctx.Tick, newEntityMoveAnimation(s.player, to)) + } +} + +func (s *playLevel) Handle(e allg5.Event) { + switch e := e.(type) { + case *allg5.KeyCharEvent: + switch e.KeyCode { + case allg5.KeyUp: + s.tryPlayerMove(s.player.pos.Add2D(0, -1)) + case allg5.KeyRight: + s.tryPlayerMove(s.player.pos.Add2D(1, 0)) + case allg5.KeyDown: + s.tryPlayerMove(s.player.pos.Add2D(0, 1)) + case allg5.KeyLeft: + s.tryPlayerMove(s.player.pos.Add2D(-1, 0)) + } + } } func (s *playLevel) Render(ctx *alui.Context, bounds geom.RectangleF32) { + basicTile := s.ctx.Textures["basic_tile"] waterTile := s.ctx.Textures["water_tile"] tileBmp := func(t tile) Texture { @@ -70,15 +167,15 @@ func (s *playLevel) Render(ctx *alui.Context, bounds geom.RectangleF32) { villain := s.ctx.Textures["villain_character"] brick := s.ctx.Textures["brick"] crate := s.ctx.Textures["crate"] - entityBmp := func(e entity) Texture { + entityBmp := func(e entityType) Texture { switch e { - case entityCharacter: + case entityTypeCharacter: return character - case entityVillain: + case entityTypeVillain: return villain - case entityBrick: + case entityTypeBrick: return brick - case entityCrate: + case entityTypeCrate: return crate default: return Texture{} @@ -96,25 +193,31 @@ func (s *playLevel) Render(ctx *alui.Context, bounds geom.RectangleF32) { continue } pos := geom.Pt(i%level.width, i/level.width) - screenPos := s.toScreenPos(pos) + srcPos := s.posToScreen(pos) if t == tileWater { - screenPos.Y += 8 * s.scale + srcPos.Y += 8 * s.scale } - tile.DrawOptions(screenPos.X, screenPos.Y, allg5.DrawOptions{Center: true, Scale: allg5.NewUniformScale(s.scale * scale)}) + tile.DrawOptions(srcPos.X, srcPos.Y, allg5.DrawOptions{Center: true, Scale: allg5.NewUniformScale(s.scale * scale)}) } - for i, e := range level.entities { - bmp := entityBmp(e) + var entities []*entity + entities = append(entities, s.player) + entities = append(entities, s.bricks...) + entities = append(entities, s.sunken...) + + sort.Slice(entities, func(i, j int) bool { + if entities[i].scr.Y == entities[j].scr.Y { + return entities[i].scr.X < entities[j].scr.X + } + return entities[i].scr.Y < entities[j].scr.Y + }) + + for _, e := range entities { + bmp := entityBmp(e.typ) if bmp.Bitmap == nil { continue } - pos := geom.Pt(i%level.width, i/level.width) - screenPos := s.toScreenPos(pos) - screenPos.Y -= 48 * s.scale - // scale := scale - // if e == entityCharacter { - // scale *= .4 - // } - bmp.DrawOptions(screenPos.X, screenPos.Y, allg5.DrawOptions{Center: true, Scale: allg5.NewUniformScale(s.scale * scale)}) + scrPos := s.posToScreenF32(e.scr) + bmp.DrawOptions(scrPos.X, scrPos.Y, allg5.DrawOptions{Center: true, Scale: allg5.NewUniformScale(s.scale * scale)}) } } diff --git a/gut/animation.go b/gut/animation.go new file mode 100644 index 0000000..b4864bf --- /dev/null +++ b/gut/animation.go @@ -0,0 +1,34 @@ +package gut + +import "time" + +type Animation interface { + Animate(start, now time.Duration) bool +} + +type Animations struct { + anis []animation +} + +type animation struct { + start time.Duration + ani Animation +} + +func (a *Animations) Idle() bool { + return len(a.anis) == 0 +} + +func (a *Animations) Start(now time.Duration, ani Animation) { + a.anis = append(a.anis, animation{now, ani}) +} + +func (a *Animations) Animate(now time.Duration) { + active := make([]animation, 0, len(a.anis)) + for _, ani := range a.anis { + if ani.ani.Animate(ani.start, now) { + active = append(active, ani) + } + } + a.anis = active +}