1
1
mirror of https://github.com/OpenDiablo2/OpenDiablo2 synced 2024-09-27 21:56:19 -04:00
OpenDiablo2/d2core/d2map/d2mapentity/factory.go
presiyan-ivanov 88326b5278
Initial cast overlay implementation. Fix HeroSkill deserialization & map entities processing crashing for remote client. (#766)
* Casting a skill now plays the corresponding overlay(if any).

* Prevent a crash caused by nil pointer in HeroSkill deserialization, happening when
unmarshalling HeroSkill from packets as a remote client.

* Add PlayerAnimationModeNone to handle some of the Skills(e.g.
Paladin auras) having "" as animation mode.

* Joining a game as remote client now waits for map generation to finish
before rendering map or processing map entities. This is temporary hack to prevent the game from
crashing due to concurrent map read & write exception.

* Send CastSkill packet to other clients.

Co-authored-by: Presiyan Ivanov <presiyan-ivanov@users.noreply.github.com>
2020-10-10 18:47:51 -04:00

290 lines
8.3 KiB
Go

package d2mapentity
import (
"fmt"
"github.com/google/uuid"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2enum"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2fileformats/d2tbl"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2interface"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2math/d2vector"
"github.com/OpenDiablo2/OpenDiablo2/d2common/d2resource"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2asset"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2hero"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2inventory"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2item/diablo2item"
"github.com/OpenDiablo2/OpenDiablo2/d2core/d2records"
)
// NewMapEntityFactory creates a MapEntityFactory instance with the given asset manager
func NewMapEntityFactory(asset *d2asset.AssetManager) (*MapEntityFactory, error) {
itemFactory, err := diablo2item.NewItemFactory(asset)
if err != nil {
return nil, err
}
stateFactory, err := d2hero.NewHeroStateFactory(asset)
if err != nil {
return nil, err
}
entityFactory := &MapEntityFactory{
stateFactory,
asset,
itemFactory,
}
return entityFactory, nil
}
// MapEntityFactory creates map entities for the MapEngine
type MapEntityFactory struct {
*d2hero.HeroStateFactory
asset *d2asset.AssetManager
item *diablo2item.ItemFactory
}
// NewAnimatedEntity creates an instance of AnimatedEntity
func NewAnimatedEntity(x, y int, animation d2interface.Animation) *AnimatedEntity {
entity := &AnimatedEntity{
mapEntity: newMapEntity(x, y),
animation: animation,
}
entity.mapEntity.directioner = entity.rotate
return entity
}
// NewPlayer creates a new player entity and returns a pointer to it.
func (f *MapEntityFactory) NewPlayer(id, name string, x, y, direction int, heroType d2enum.Hero,
stats *d2hero.HeroStatsState, skills map[int]*d2hero.HeroSkill, equipment *d2inventory.CharacterEquipment) *Player {
layerEquipment := &[d2enum.CompositeTypeMax]string{
d2enum.CompositeTypeHead: equipment.Head.GetArmorClass(),
d2enum.CompositeTypeTorso: equipment.Torso.GetArmorClass(),
d2enum.CompositeTypeLegs: equipment.Legs.GetArmorClass(),
d2enum.CompositeTypeRightArm: equipment.RightArm.GetArmorClass(),
d2enum.CompositeTypeLeftArm: equipment.LeftArm.GetArmorClass(),
d2enum.CompositeTypeRightHand: equipment.RightHand.GetItemCode(),
d2enum.CompositeTypeLeftHand: equipment.LeftHand.GetItemCode(),
d2enum.CompositeTypeShield: equipment.Shield.GetItemCode(),
}
composite, err := f.asset.LoadComposite(d2enum.ObjectTypePlayer, heroType.GetToken(),
d2resource.PaletteUnits)
if err != nil {
panic(err)
}
stats.NextLevelExp = f.asset.Records.GetExperienceBreakpoint(heroType, stats.Level)
stats.Stamina = stats.MaxStamina
defaultCharStats := f.asset.Records.Character.Stats[heroType]
statsState := f.HeroStateFactory.CreateHeroStatsState(heroType, defaultCharStats)
heroState, _ := f.CreateHeroState(name, heroType, statsState)
attackSkillID := 0
result := &Player{
mapEntity: newMapEntity(x, y),
composite: composite,
Equipment: equipment,
Stats: heroState.Stats,
Skills: heroState.Skills,
//TODO: active left & right skill should be loaded from save file instead
LeftSkill: heroState.Skills[attackSkillID],
RightSkill: heroState.Skills[attackSkillID],
name: name,
Class: heroType,
//nameLabel: d2ui.NewLabel(d2resource.FontFormal11, d2resource.PaletteStatic),
isRunToggled: false,
isInTown: true,
isRunning: false,
}
result.mapEntity.uuid = id
result.SetSpeed(baseRunSpeed)
result.mapEntity.directioner = result.rotate
err = composite.SetMode(d2enum.PlayerAnimationModeTownNeutral, equipment.RightHand.GetWeaponClass())
if err != nil {
panic(err)
}
composite.SetDirection(direction)
if err := composite.Equip(layerEquipment); err != nil {
fmt.Printf("failed to equip, err: %v\n", err)
}
return result
}
// NewMissile creates a new Missile and initializes it's animation.
func (f *MapEntityFactory) NewMissile(x, y int, record *d2records.MissileRecord) (*Missile, error) {
animation, err := f.asset.LoadAnimation(
fmt.Sprintf("%s/%s.dcc", d2resource.MissileData, record.Animation.CelFileName),
d2resource.PaletteUnits,
)
if err != nil {
return nil, err
}
if record.Animation.HasSubLoop {
animation.SetSubLoop(record.Animation.SubStartingFrame, record.Animation.SubEndingFrame)
}
animation.SetEffect(d2enum.DrawEffectModulate)
animation.SetPlayLoop(record.Animation.LoopAnimation)
animation.PlayForward()
entity := NewAnimatedEntity(x, y, animation)
result := &Missile{
AnimatedEntity: entity,
record: record,
}
result.Speed = float64(record.Velocity)
return result, nil
}
// NewItem creates an item map entity
func (f *MapEntityFactory) NewItem(x, y int, codes ...string) (*Item, error) {
item, err := f.item.NewItem(codes...)
if err != nil {
return nil, err
}
filename := item.CommonRecord().FlippyFile
filepath := fmt.Sprintf("%s/%s.DC6", d2resource.ItemGraphics, filename)
animation, err := f.asset.LoadAnimation(filepath, d2resource.PaletteUnits)
if err != nil {
return nil, err
}
animation.PlayForward()
animation.SetPlayLoop(false)
entity := NewAnimatedEntity(x*5, y*5, animation)
result := &Item{
AnimatedEntity: entity,
Item: item,
}
return result, nil
}
// NewNPC creates a new NPC and returns a pointer to it.
func (f *MapEntityFactory) NewNPC(x, y int, monstat *d2records.MonStatsRecord, direction int) (*NPC, error) {
result := &NPC{
mapEntity: newMapEntity(x, y),
HasPaths: false,
monstatRecord: monstat,
monstatEx: f.asset.Records.Monster.Stats2[monstat.ExtraDataKey],
}
var equipment [16]string
for compType, opts := range result.monstatEx.EquipmentOptions {
equipment[compType] = selectEquip(opts)
}
composite, err := f.asset.LoadComposite(d2enum.ObjectTypeCharacter, monstat.AnimationDirectoryToken,
d2resource.PaletteUnits)
if err != nil {
return nil, err
}
result.composite = composite
if err := composite.SetMode(d2enum.MonsterAnimationModeNeutral,
result.monstatEx.BaseWeaponClass); err != nil {
return nil, err
}
if err := composite.Equip(&equipment); err != nil {
return nil, err
}
result.SetSpeed(float64(monstat.SpeedBase))
result.mapEntity.directioner = result.rotate
result.composite.SetDirection(direction)
if result.monstatRecord != nil && result.monstatRecord.IsInteractable {
result.name = d2tbl.TranslateString(result.monstatRecord.NameString)
}
return result, nil
}
// NewCastOverlay creates a cast overlay map entity
func (f *MapEntityFactory) NewCastOverlay(x, y int, overlayRecord *d2records.OverlayRecord) (*CastOverlay, error) {
animation, err := f.asset.LoadAnimationWithEffect(
fmt.Sprintf("/data/Global/Overlays/%s.dcc", overlayRecord.Filename),
d2resource.PaletteUnits,
d2enum.DrawEffectModulate,
)
// TODO: Frame index and played count seem to be shared across the cloned animation objects when we retrieve the animation from the asset manager cache.
animation.Rewind()
animation.ResetPlayedCount()
if err != nil {
return nil, err
}
animationSpeed := float64(overlayRecord.AnimRate*25.0) / 1000.0
playLoop := false // TODO: should be based on the overlay record, some overlays can repeat(e.g. Bone Shield, Frozen Armor)
animation.SetPlayLength(animationSpeed)
animation.SetPlayLoop(playLoop)
animation.PlayForward()
targetX := x + overlayRecord.XOffset
targetY := y + overlayRecord.YOffset
entity := NewAnimatedEntity(targetX, targetY, animation)
result := &CastOverlay{
AnimatedEntity: entity,
record: overlayRecord,
playLoop: playLoop,
}
return result, nil
}
// NewObject creates an instance of AnimatedComposite
func (f *MapEntityFactory) NewObject(x, y int, objectRec *d2records.ObjectDetailsRecord,
palettePath string) (*Object, error) {
locX, locY := float64(x), float64(y)
entity := &Object{
uuid: uuid.New().String(),
objectRecord: objectRec,
Position: d2vector.NewPosition(locX, locY),
name: d2tbl.TranslateString(objectRec.Name),
}
objectType := f.asset.Records.Object.Types[objectRec.Index]
composite, err := f.asset.LoadComposite(d2enum.ObjectTypeItem, objectType.Token,
palettePath)
if err != nil {
return nil, err
}
entity.composite = composite
err = entity.setMode(d2enum.ObjectAnimationModeNeutral, 0, false)
if err != nil {
return nil, err
}
_, err = initObject(entity)
if err != nil {
return nil, err
}
return entity, nil
}