From 2835ff4cf16822b50d51834384c83595b4395036 Mon Sep 17 00:00:00 2001 From: presiyan-ivanov <15377841+presiyan-ivanov@users.noreply.github.com> Date: Mon, 22 Jun 2020 22:55:32 +0300 Subject: [PATCH] =?UTF-8?q?Improve=20run/walk/neutral=20animation=20handli?= =?UTF-8?q?ng.=20Initial=20parsing=20of=20Level=E2=80=A6=20(#372)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Improve run/walk/neutral animation handling. Initial parsing of LevelDetail records. Support for holding mouse buttons. - Run/walk/neutral positions now map to a different animation mode(and speed) depending if in or out of town. - Run/walk toggle which can be activated/deactivated with R key. - Temporary(and incorrect) loading and mapping for LevelDetails records. - Zone change label which shows the level name from LevelDetailsRecord when the player enters a different zone. - Allow holding mouse left/right button to repeatedly generate an action. * Remove duplicate load of LevelDetails. Replace numbers in zone change logic with their corresponding RegionIdType * Move zone change check at the correct place Co-authored-by: Presiyan Ivanov --- d2core/d2input/d2input.go | 4 + d2core/d2input/input_manager.go | 13 ++ .../d2map/d2mapentity/animated_composite.go | 54 ++++++-- d2core/d2map/d2mapentity/map_entity.go | 8 ++ d2core/d2map/d2mapentity/player.go | 32 +++++ d2game/d2gamescreen/game.go | 27 +++- d2game/d2player/game_controls.go | 127 +++++++++++++----- d2networking/d2client/game_client.go | 5 +- 8 files changed, 215 insertions(+), 55 deletions(-) diff --git a/d2core/d2input/d2input.go b/d2core/d2input/d2input.go index a6c47b5a..85814136 100644 --- a/d2core/d2input/d2input.go +++ b/d2core/d2input/d2input.go @@ -197,6 +197,10 @@ type MouseButtonDownHandler interface { OnMouseButtonDown(event MouseEvent) bool } +type MouseButtonRepeatHandler interface { + OnMouseButtonRepeat(event MouseEvent) bool +} + type MouseButtonUpHandler interface { OnMouseButtonUp(event MouseEvent) bool } diff --git a/d2core/d2input/input_manager.go b/d2core/d2input/input_manager.go index f0007221..6f0abf3c 100644 --- a/d2core/d2input/input_manager.go +++ b/d2core/d2input/input_manager.go @@ -126,6 +126,19 @@ func (im *inputManager) advance(elapsed float64) error { }) } + for button := ebiten.MouseButtonLeft; button < ebiten.MouseButtonMiddle; button++ { + if ebiten.IsMouseButtonPressed(button) { + event := MouseEvent{eventBase, MouseButton(button)} + im.propagate(func(handler Handler) bool { + if l, ok := handler.(MouseButtonRepeatHandler); ok { + return l.OnMouseButtonRepeat(event) + } + + return false + }) + } + } + if inpututil.IsMouseButtonJustReleased(button) { event := MouseEvent{eventBase, MouseButton(button)} im.propagate(func(handler Handler) bool { diff --git a/d2core/d2map/d2mapentity/animated_composite.go b/d2core/d2map/d2mapentity/animated_composite.go index 41ce7175..f3e551fe 100644 --- a/d2core/d2map/d2mapentity/animated_composite.go +++ b/d2core/d2map/d2mapentity/animated_composite.go @@ -68,25 +68,51 @@ func (ac *AnimatedComposite) Render(target d2render.Surface) { // rotate sets direction and changes animation func (ac *AnimatedComposite) rotate(angle float64) { - // TODO: Check if is in town and if is player. - newAnimationMode := ac.composite.GetAnimationMode() - if !ac.IsAtTarget() { - if ac.player != nil { - if ac.player.IsInTown() { - newAnimationMode = d2enum.AnimationModePlayerTownWalk.String() - } else { - newAnimationMode = d2enum.AnimationModePlayerWalk.String() - } - } else { - newAnimationMode = d2enum.AnimationModeMonsterWalk.String() - } - } - + newAnimationMode := ac.GetAnimationMode().String() newDirection := angleToDirection(angle) + if newAnimationMode != ac.composite.GetAnimationMode() || newDirection != ac.direction { ac.SetMode(newAnimationMode, ac.weaponClass, newDirection) } +} +func (ac *AnimatedComposite) GetAnimationMode() d2enum.AnimationMode { + var newAnimationMode d2enum.AnimationMode + if ac.player != nil { + newAnimationMode = ac.GetPlayerAnimationMode() + } else { + newAnimationMode = ac.GetMonsterAnimationMode() + } + + return newAnimationMode +} + +func (ac *AnimatedComposite) GetPlayerAnimationMode() d2enum.AnimationMode { + if ac.player.IsRunning() && !ac.IsAtTarget(){ + return d2enum.AnimationModePlayerRun + } + + if ac.player.IsInTown() { + if !ac.IsAtTarget() { + return d2enum.AnimationModePlayerTownWalk + } + + return d2enum.AnimationModePlayerTownNeutral + } + + if !ac.IsAtTarget() { + return d2enum.AnimationModePlayerWalk + } + + return d2enum.AnimationModePlayerNeutral +} + +func (ac *AnimatedComposite) GetMonsterAnimationMode() d2enum.AnimationMode { + if !ac.IsAtTarget() { + return d2enum.AnimationModeMonsterWalk + } + + return d2enum.AnimationModeMonsterNeutral } func (ac *AnimatedComposite) Advance(elapsed float64) { diff --git a/d2core/d2map/d2mapentity/map_entity.go b/d2core/d2map/d2mapentity/map_entity.go index 5b367c31..76a2aa47 100644 --- a/d2core/d2map/d2mapentity/map_entity.go +++ b/d2core/d2map/d2mapentity/map_entity.go @@ -53,6 +53,14 @@ func (m *mapEntity) SetPath(path []astar.Pather, done func()) { m.done = done } +func (m *mapEntity) SetSpeed(speed float64) { + m.Speed = speed +} + +func (m *mapEntity) GetSpeed() float64 { + return m.Speed +} + func (m *mapEntity) getStepLength(tickTime float64) (float64, float64) { length := tickTime * m.Speed diff --git a/d2core/d2map/d2mapentity/player.go b/d2core/d2map/d2mapentity/player.go index fb58387f..e0281cd5 100644 --- a/d2core/d2map/d2mapentity/player.go +++ b/d2core/d2map/d2mapentity/player.go @@ -21,8 +21,14 @@ type Player struct { lastPathSize int isInTown bool animationMode string + isRunToggled bool + isRunning bool } +// run speed should be walkspeed * 1.5, since in the original game it is 6 yards walk and 9 yards run. +var baseWalkSpeed = 6.0 +var baseRunSpeed = 9.0 + func CreatePlayer(id, name string, x, y int, direction int, heroType d2enum.Hero, equipment d2inventory.CharacterEquipment) *Player { object := &d2datadict.ObjectLookupRecord{ Mode: d2enum.AnimationModePlayerTownNeutral.String(), @@ -44,6 +50,7 @@ func CreatePlayer(id, name string, x, y int, direction int, heroType d2enum.Hero if err != nil { panic(err) } + entity.SetSpeed(baseRunSpeed) result := &Player{ Id: id, @@ -52,6 +59,9 @@ func CreatePlayer(id, name string, x, y int, direction int, heroType d2enum.Hero direction: direction, Name: name, nameLabel: d2ui.CreateLabel(d2resource.FontFormal11, d2resource.PaletteStatic), + isRunToggled: true, + isInTown: true, + isRunning: true, } result.nameLabel.Alignment = d2ui.LabelAlignCenter result.nameLabel.SetText(name) @@ -68,6 +78,28 @@ func (p *Player) SetIsInTown(isInTown bool) { p.isInTown = isInTown } +func (p *Player) ToggleRunWalk() { + p.isRunToggled = !p.isRunToggled +} + +func (p *Player) IsRunToggled() bool { + return p.isRunToggled +} + +func (p *Player) IsRunning() bool { + return p.isRunning +} + +func (p *Player) SetIsRunning(isRunning bool) { + p.isRunning = isRunning + + if isRunning { + p.SetSpeed(baseRunSpeed) + } else { + p.SetSpeed(baseWalkSpeed) + } +} + func (p Player) IsInTown() bool { return p.isInTown } diff --git a/d2game/d2gamescreen/game.go b/d2game/d2gamescreen/game.go index 150038c0..dc8a4671 100644 --- a/d2game/d2gamescreen/game.go +++ b/d2game/d2gamescreen/game.go @@ -1,13 +1,15 @@ package d2gamescreen import ( + "fmt" "image/color" - "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapentity" - "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2maprenderer" - + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict" + "github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2audio" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2input" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2mapentity" + "github.com/OpenDiablo2/OpenDiablo2/d2core/d2map/d2maprenderer" "github.com/OpenDiablo2/OpenDiablo2/d2core/d2render" "github.com/OpenDiablo2/OpenDiablo2/d2game/d2player" "github.com/OpenDiablo2/OpenDiablo2/d2networking/d2client" @@ -22,7 +24,7 @@ type Game struct { mapRenderer *d2maprenderer.MapRenderer gameControls *d2player.GameControls // TODO: Hack localPlayer *d2mapentity.Player - lastLevelType int + lastRegionType d2enum.RegionIdType ticksSinceLevelCheck float64 } @@ -31,7 +33,7 @@ func CreateGame(gameClient *d2client.GameClient) *Game { gameClient: gameClient, gameControls: nil, localPlayer: nil, - lastLevelType: -1, + lastRegionType: d2enum.RegionNone, ticksSinceLevelCheck: 0, mapRenderer: d2maprenderer.CreateMapRenderer(gameClient.MapEngine), } @@ -61,6 +63,8 @@ func (v *Game) Render(screen d2render.Surface) error { return nil } +var hideZoneTextAfterSeconds = 2.0 + func (v *Game) Advance(tickTime float64) error { if !v.gameControls.InEscapeMenu() || len(v.gameClient.Players) != 1 { v.gameClient.MapEngine.Advance(tickTime) // TODO: Hack @@ -77,13 +81,22 @@ func (v *Game) Advance(tickTime float64) error { tile := v.gameClient.MapEngine.TileAt(v.localPlayer.TileX, v.localPlayer.TileY) if tile != nil { switch tile.RegionType { - case 1: // Rogue encampent + case d2enum.RegionAct1Town: // Rogue encampent v.localPlayer.SetIsInTown(true) d2audio.PlayBGM("/data/global/music/Act1/town1.wav") - case 2: // Blood Moore + case d2enum.RegionAct1Wilderness: // Blood Moore v.localPlayer.SetIsInTown(false) d2audio.PlayBGM("/data/global/music/Act1/wild.wav") } + + // skip showing zone change text the first time we enter the world + if v.lastRegionType != d2enum.RegionNone && v.lastRegionType != tile.RegionType { + //TODO: Should not be using RegionType as an index - this will return incorrect LevelDetails record for most of the zones. + v.gameControls.SetZoneChangeText(fmt.Sprintf("Entering The %s", d2datadict.LevelDetails[int(tile.RegionType)].LevelDisplayName)) + v.gameControls.ShowZoneChangeText() + v.gameControls.HideZoneChangeTextAfter(hideZoneTextAfterSeconds) + } + v.lastRegionType = tile.RegionType } } } diff --git a/d2game/d2player/game_controls.go b/d2game/d2player/game_controls.go index 9780a95e..48ae4fe2 100644 --- a/d2game/d2player/game_controls.go +++ b/d2game/d2player/game_controls.go @@ -1,6 +1,9 @@ package d2player import ( + "image/color" + "time" + "github.com/OpenDiablo2/OpenDiablo2/d2common" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2data/d2datadict" "github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource" @@ -35,10 +38,12 @@ type GameControls struct { FreeCam bool // UI - globeSprite *d2ui.Sprite - mainPanel *d2ui.Sprite - menuButton *d2ui.Sprite - skillIcon *d2ui.Sprite + globeSprite *d2ui.Sprite + mainPanel *d2ui.Sprite + menuButton *d2ui.Sprite + skillIcon *d2ui.Sprite + zoneChangeText *d2ui.Label + isZoneTextShown bool } func NewGameControls(hero *d2mapentity.Player, mapEngine *d2mapengine.MapEngine, mapRenderer *d2maprenderer.MapRenderer, inputListener InputCallbackListener) *GameControls { @@ -46,14 +51,19 @@ func NewGameControls(hero *d2mapentity.Player, mapEngine *d2mapengine.MapEngine, missileID = id }) + label := d2ui.CreateLabel(d2resource.Font30, d2resource.PaletteUnits) + label.Color = color.RGBA{R: 255, G: 88, B: 82, A: 255} + label.Alignment = d2ui.LabelAlignCenter + gc := &GameControls{ - hero: hero, - mapEngine: mapEngine, - inputListener: inputListener, - mapRenderer: mapRenderer, - inventory: NewInventory(), - heroStats: NewHeroStats(), - escapeMenu: NewEscapeMenu(), + hero: hero, + mapEngine: mapEngine, + inputListener: inputListener, + mapRenderer: mapRenderer, + inventory: NewInventory(), + heroStats: NewHeroStats(), + escapeMenu: NewEscapeMenu(), + zoneChangeText: &label, } d2term.BindAction("freecam", "toggle free camera movement", func() { @@ -116,9 +126,37 @@ func (g *GameControls) OnKeyDown(event d2input.KeyEvent) bool { case d2input.KeyC: g.heroStats.Toggle() g.updateLayout() + case d2input.KeyR: + g.hero.ToggleRunWalk() + // TODO: change the running menu icon + g.hero.SetIsRunning(g.hero.IsRunToggled()) default: return false } + return false +} + +var lastLeftBtnActionTime float64 = 0 +var lastRightBtnActionTime float64 = 0 +var mouseBtnActionsTreshhold = 0.25 + +func (g *GameControls) OnMouseButtonRepeat(event d2input.MouseEvent) bool { + px, py := g.mapRenderer.ScreenToWorld(event.X, event.Y) + px = float64(int(px*10)) / 10.0 + py = float64(int(py*10)) / 10.0 + + now := d2common.Now() + if event.Button == d2input.MouseButtonLeft && now-lastLeftBtnActionTime >= mouseBtnActionsTreshhold { + lastLeftBtnActionTime = now + g.inputListener.OnPlayerMove(px, py) + return true + } + + if event.Button == d2input.MouseButtonRight && now-lastRightBtnActionTime >= mouseBtnActionsTreshhold { + lastRightBtnActionTime = now + g.ShootMissile(px, py) + return true + } return true } @@ -138,37 +176,44 @@ func (g *GameControls) OnMouseButtonDown(event d2input.MouseEvent) bool { py = float64(int(py*10)) / 10.0 if event.Button == d2input.MouseButtonLeft { + lastLeftBtnActionTime = d2common.Now() g.inputListener.OnPlayerMove(px, py) return true } if event.Button == d2input.MouseButtonRight { - missile, err := d2mapentity.CreateMissile( - int(g.hero.AnimatedComposite.LocationX), - int(g.hero.AnimatedComposite.LocationY), - d2datadict.Missiles[missileID], - ) - if err != nil { - return false - } - - rads := d2common.GetRadiansBetween( - g.hero.AnimatedComposite.LocationX, - g.hero.AnimatedComposite.LocationY, - px*5, - py*5, - ) - missile.SetRadians(rads, func() { - g.mapEngine.RemoveEntity(missile) - }) - - g.mapEngine.AddEntity(missile) - return true + lastRightBtnActionTime = d2common.Now() + return g.ShootMissile(px, py) } return false } +func (g *GameControls) ShootMissile(px float64, py float64) bool { + missile, err := d2mapentity.CreateMissile( + int(g.hero.AnimatedComposite.LocationX), + int(g.hero.AnimatedComposite.LocationY), + d2datadict.Missiles[missileID], + ) + if err != nil { + return false + } + + rads := d2common.GetRadiansBetween( + g.hero.AnimatedComposite.LocationX, + g.hero.AnimatedComposite.LocationY, + px*5, + py*5, + ) + + missile.SetRadians(rads, func() { + g.mapEngine.RemoveEntity(missile) + }) + + g.mapEngine.AddEntity(missile) + return true +} + func (g *GameControls) Load() { animation, _ := d2asset.LoadAnimation(d2resource.GameGlobeOverlap, d2resource.PaletteSky) g.globeSprite, _ = d2ui.LoadSprite(animation) @@ -290,6 +335,24 @@ func (g *GameControls) Render(target d2render.Surface) { g.globeSprite.SetPosition(offset+8, height-8) g.globeSprite.Render(target) + if g.isZoneTextShown { + g.zoneChangeText.SetPosition(width/2, height/4) + g.zoneChangeText.Render(target) + } +} + +func (g *GameControls) SetZoneChangeText(text string) { + g.zoneChangeText.SetText(text) +} + +func (g *GameControls) ShowZoneChangeText() { + g.isZoneTextShown = true +} + +func (g *GameControls) HideZoneChangeTextAfter(delay float64) { + time.AfterFunc(time.Duration(delay)*time.Second, func() { + g.isZoneTextShown = false + }) } func (g *GameControls) InEscapeMenu() bool { diff --git a/d2networking/d2client/game_client.go b/d2networking/d2client/game_client.go index 08cf8bdc..aa932296 100644 --- a/d2networking/d2client/game_client.go +++ b/d2networking/d2client/game_client.go @@ -95,10 +95,11 @@ func (g *GameClient) OnPacketReceived(packet d2netpacket.NetPacket) error { regionType := tile.RegionType if regionType == d2enum.RegionAct1Town { - player.AnimatedComposite.SetAnimationMode(d2enum.AnimationModePlayerTownNeutral.String()) + player.SetIsInTown(true) } else { - player.AnimatedComposite.SetAnimationMode(d2enum.AnimationModePlayerNeutral.String()) + player.SetIsInTown(false) } + player.AnimatedComposite.SetAnimationMode(player.GetAnimationMode().String()) }) } default: