Improve run/walk/neutral animation handling. Initial parsing of Level… (#372)

* 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 <presiyan-ivanov@users.noreply.github.com>
This commit is contained in:
presiyan-ivanov 2020-06-22 22:55:32 +03:00 committed by GitHub
parent 8dcec1f209
commit 2835ff4cf1
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
8 changed files with 215 additions and 55 deletions

View File

@ -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
}

View File

@ -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 {

View File

@ -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) {

View File

@ -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

View File

@ -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
}

View File

@ -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
}
}
}

View File

@ -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 {

View File

@ -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: