From c4b128ac2e839888e0c4bc44ed56d25b278d9806 Mon Sep 17 00:00:00 2001 From: presiyan-ivanov <15377841+presiyan-ivanov@users.noreply.github.com> Date: Sat, 24 Oct 2020 17:08:45 +0300 Subject: [PATCH] Handle casting summon skills and skills that have multiple missile references. Add background to skill select's skill hover tooltip. (#786) Co-authored-by: Presiyan Ivanov --- d2core/d2map/d2mapentity/player.go | 1 + d2game/d2player/skill_select_panel.go | 33 +++++++++---- d2networking/d2client/game_client.go | 71 ++++++++++++++++++++++++--- 3 files changed, 88 insertions(+), 17 deletions(-) diff --git a/d2core/d2map/d2mapentity/player.go b/d2core/d2map/d2mapentity/player.go index e3c25347..5c50133e 100644 --- a/d2core/d2map/d2mapentity/player.go +++ b/d2core/d2map/d2mapentity/player.go @@ -213,6 +213,7 @@ func (p *Player) IsCasting() bool { // StartCasting sets a flag indicating the player is casting a skill and // sets the animation mode to the casting animation. // This handles all types of skills - melee, ranged, kick, summon, etc. +// NB: onFinishedCasting is called when the casting animation is >50% complete func (p *Player) StartCasting(animMode d2enum.PlayerAnimationMode, onFinishedCasting func()) { // passive skills, auras, etc. if animMode == d2enum.PlayerAnimationModeNone { diff --git a/d2game/d2player/skill_select_panel.go b/d2game/d2player/skill_select_panel.go index de8f9966..b5965787 100644 --- a/d2game/d2player/skill_select_panel.go +++ b/d2game/d2player/skill_select_panel.go @@ -2,6 +2,7 @@ package d2player import ( "fmt" + "image/color" "log" "sort" @@ -25,6 +26,9 @@ const ( leftPanelStartX = 90 skillPanelOffsetY = 465 skillListsLength = 5 // 0 to 4. 0 - General Skills, 1 to 3 - Class-specific skills(based on the 3 different skill trees), 4 - Other skills + tooltipPadLeft = 3 + tooltipPadRight = 3 + tooltipPadBottom = 5 ) // SkillPanel represents a skill select menu popup that is displayed when the player left clicks on his active left/right skill. @@ -39,8 +43,7 @@ type SkillPanel struct { renderer d2interface.Renderer ui *d2ui.UIManager hoveredSkill *d2hero.HeroSkill - hoverTooltipPos d2geom.Point - //TODO: should be a cached image which contains the skill text + the tooltip background + hoverTooltipRect *d2geom.Rectangle hoverTooltipText *d2ui.Label } @@ -132,9 +135,14 @@ func (s *SkillPanel) Render(target d2interface.Surface) error { } if s.hoveredSkill != nil { - target.PushTranslation(s.hoverTooltipPos.X, s.hoverTooltipPos.Y) + target.PushTranslation(s.hoverTooltipRect.Left, s.hoverTooltipRect.Top) + target.DrawRect(s.hoverTooltipRect.Width, s.hoverTooltipRect.Height, color.RGBA{0, 0, 0, uint8(200)}) + + // the text should be centered horizontally in the tooltip rect + target.PushTranslation(s.hoverTooltipRect.Width/2, 0) s.hoverTooltipText.Render(target) - target.Pop() + + target.PopN(2) } return nil @@ -326,19 +334,26 @@ func (s *SkillPanel) HandleMouseMove(X int, Y int) bool { if previousHovered != s.hoveredSkill && s.hoveredSkill != nil { skillDescription := d2tbl.TranslateString(s.hoveredSkill.ShortKey) - //TODO: should generate a cached image for the tooltip instead s.hoverTooltipText.SetText(fmt.Sprintf("%s\n%s", s.hoveredSkill.Skill, skillDescription)) listRow := s.GetListRowByPos(X, Y) - tooltipWidth, _ := s.hoverTooltipText.GetSize() - tooltipX := (s.getSkillIdxAtPos(X, Y) * skillIconWidth) + s.getRowStartX(listRow) + (tooltipWidth / 2) + textWidth, textHeight := s.hoverTooltipText.GetSize() + + tooltipX := (s.getSkillIdxAtPos(X, Y) * skillIconWidth) + s.getRowStartX(listRow) + tooltipWidth := textWidth + tooltipPadLeft + tooltipPadRight + if tooltipX+tooltipWidth >= screenWidth { - tooltipX = screenWidth - (tooltipWidth / 2) + tooltipX = screenWidth - tooltipWidth } tooltipY := listRow.Rectangle.Top + listRow.Rectangle.Height + tooltipHeight := textHeight + tooltipPadBottom - s.hoverTooltipPos = d2geom.Point{X: tooltipX, Y: tooltipY} + s.hoverTooltipRect = &d2geom.Rectangle{ + Left: tooltipX, + Top: tooltipY, + Width: tooltipWidth, + Height: tooltipHeight} } return true diff --git a/d2networking/d2client/game_client.go b/d2networking/d2client/game_client.go index 3282aa07..54aa6441 100644 --- a/d2networking/d2client/game_client.go +++ b/d2networking/d2client/game_client.go @@ -280,15 +280,31 @@ func (g *GameClient) handleCastSkillPacket(packet d2netpacket.NetPacket) error { skillRecord := g.asset.Records.Skill.Details[playerCast.SkillID] - missileEntity, err := g.createMissileEntity(skillRecord, player, castX, castY) + missileEntities, err := g.createMissileEntities(skillRecord, player, castX, castY) if err != nil { return err } + var summonedNpcEntity *d2mapentity.NPC + if skillRecord.Summon != "" { + summonedNpcEntity, err = g.createSummonedNpcEntity(skillRecord, int(castX), int(castY)) + + if err != nil { + return err + } + } + player.StartCasting(skillRecord.Anim, func() { - if missileEntity != nil { - // shoot the missile after the player has finished casting - g.MapEngine.AddEntity(missileEntity) + if len(missileEntities) > 0 { + // shoot the missiles of the skill after the player has finished casting + for _, missileEntity := range missileEntities { + g.MapEngine.AddEntity(missileEntity) + } + } + + if summonedNpcEntity != nil { + // summon the referenced NPC after the player has finished casting + g.MapEngine.AddEntity(summonedNpcEntity) } }) @@ -298,14 +314,53 @@ func (g *GameClient) handleCastSkillPacket(packet d2netpacket.NetPacket) error { return nil } -func (g *GameClient) createMissileEntity(skillRecord *d2records.SkillRecord, player *d2mapentity.Player, castX float64, castY float64) (*d2mapentity.Missile, error) { - missileRecord := g.asset.Records.GetMissileByName(skillRecord.Cltmissile) +func (g *GameClient) createSummonedNpcEntity(skillRecord *d2records.SkillRecord, X, Y int) (*d2mapentity.NPC, error) { + monsterStatsRecord := g.asset.Records.Monster.Stats[skillRecord.Summon] + + if monsterStatsRecord == nil { + return nil, fmt.Errorf("Cannot cast skill - No monstat entry for \"%s\"", skillRecord.Summon) + } + + // TODO: overlay animations for the summon + summonedNpcEntity, err := g.MapEngine.NewNPC(X, Y, monsterStatsRecord, 0) + if err != nil { + return nil, err + } + + return summonedNpcEntity, nil +} + +func (g *GameClient) createMissileEntities(skillRecord *d2records.SkillRecord, player *d2mapentity.Player, castX float64, castY float64) ([]*d2mapentity.Missile, error) { + missileRecords := []*d2records.MissileRecord{ + g.asset.Records.GetMissileByName(skillRecord.Cltmissile), + g.asset.Records.GetMissileByName(skillRecord.Cltmissilea), + g.asset.Records.GetMissileByName(skillRecord.Cltmissileb), + g.asset.Records.GetMissileByName(skillRecord.Cltmissilec), + g.asset.Records.GetMissileByName(skillRecord.Cltmissiled), + } + + missileEntities := make([]*d2mapentity.Missile, 0) + for _, missileRecord := range missileRecords { + if missileRecord == nil { + continue; + } + + missileEntity, err := g.createMissileEntity(missileRecord, player, castX, castY) + if err != nil { + return nil, err + } + + missileEntities = append(missileEntities, missileEntity) + } + + return missileEntities, nil +} + +func (g *GameClient) createMissileEntity(missileRecord *d2records.MissileRecord, player *d2mapentity.Player, castX, castY float64) (*d2mapentity.Missile, error){ if missileRecord == nil { return nil, nil } - var missileEntity *d2mapentity.Missile - radians := d2math.GetRadiansBetween( player.Position.X(), player.Position.Y(),